Text
                    
Flash® Game Development in a Social, Mobile, and 3D World Keith Gladstien Cengage Learning PTR Australia • Brazil • Japan • Korea • Mexico • Singapore • Spain • United Kingdom • United States
Flash® Game Development in a Social, Mobile, and 3D World Keith Gladstien Publisher and General Manager, Cengage Learning PTR: Stacy L. Hiquet Associate Director of Marketing: Sarah Panella Manager of Editorial Services: Heather Talbot Senior Marketing Manager: Mark Hughes © 2014 Cengage Learning PTR. ALL RIGHTS RESERVED. No part of this work covered by the copyright herein may be reproduced, transmitted, stored, or used in any form or by any means graphic, electronic, or mechanical, including but not limited to photocopying, recording, scanning, digitizing, taping, Web distribution, information networks, or information storage and retrieval systems, except as permitted under Section 107 or 108 of the 1976 United States Copyright Act, without the prior written permission of the publisher. For product information and technology assistance, contact us at Cengage Learning Customer & Sales Support, 1-800-354-9706 For permission to use material from this text or product, submit all requests online at cengage.com/permissions Senior Acquisitions Editor: Emi Smith Further permissions questions can be emailed to permissionrequest@cengage.com Project Editor: Dan Foster, Scribe Tribe Technical Reviewer: Glen Rhodes Interior Layout Tech: MPS Limited Adobe® Flash® is a registered trademark of Adobe Systems Incorporated. Cover Designer: Luke Fletcher All other trademarks are the property of their respective owners. Proofreader & Indexer: Kelly Talbot Editing Services All images © Cengage Learning unless otherwise noted. Copy Editor: Cathleen Small Library of Congress Control Number: 2013932035 ISBN-13: 978-1-4354-6020-1 ISBN-10: 1-4354-6020-0 eISBN-10:1-4354-6021-9 Cengage Learning PTR 20 Channel Center Street Boston, MA 02210 USA Cengage Learning is a leading provider of customized learning solutions with office locations around the globe, including Singapore, the United Kingdom, Australia, Mexico, Brazil, and Japan. Locate your local office at: international.cengage.com/region Cengage Learning products are represented in Canada by Nelson Education, Ltd. For your lifelong learning solutions, visit cengageptr.com Visit our corporate website at cengage.com Printed in the United States of America 1 2 3 4 5 6 7 15 14 13
To my father, Russell S. Gladstien (1918–2012).
Acknowledgments To Dan Foster, Emi Smith, Cathleen Small, Kelly Talbot, Glen Rhodes, Luke Fletcher, Mark Garvey, and everyone else involved in the publication of this book: Thank you! iv
About the Author Keith Gladstien was educated at the University of California, Irvine (B.A.), Louisiana State University in New Orleans (M.S.), Purdue University (Ph.D.), University of California, Los Angeles (postdoctoral scholar), Yale University (M.D.), and a second stint at the University of California, Los Angeles (pediatrics residency). Or, at least many tried their best to educate him. His interest in ActionScript programming started more than 10 years ago and has increased as his involvement with the Adobe ActionScript and Flash forums has expanded. He has done and continues to do freelance work for hundreds of clients worldwide. He is an Adobe Certified Expert in Flash CS6. v
Contents Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi Chapter 1 Problematic Code: Debugging and Testing . . . . . . . . . . . . . . . . . . 1 Debugging Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1 Permit Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .2 Compile-Time Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Run-Time Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Custom Profile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .8 Errors That Trigger Error Messages. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Errors That Do Not Trigger Error Messages. . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Testing and Experimenting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Final Words on Debugging. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Chapter 2 Avoiding Problematic Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Timeline Coding versus Class File Coding . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 MovieClip Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Class Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Function Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 Chapter 3 Writing Class Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 Internal, Private, Protected, and Public Properties . . . . . . . . . . . . . . . . . . . . . 31 Getters and Setters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 Example 1: Passing a Variable from Main to C1 via the C1 Constructor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 vi
Contents Example 2: Passing a Variable from Main to C1 Using a C1 Public Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 Example 3: Using an Event Dispatcher . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 static Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 Internal, Private, Protected, Public, and Static Methods . . . . . . . . . . . . . . . . . 42 Example 1: public Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 Example 2: static public Method. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 Singleton Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 dynamic Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 Chapter 4 What You Should Know. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Arrays. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Associative Arrays. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 Array Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 BitmapData . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Conditional Compiling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Dictionary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 DispatchEvent. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 ExternalInterface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 Garbage Collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 getTimer() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Listeners versus Weak Listeners . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 do Loops, for Loops, and while Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 enterFrame, setInterval(), and Timer Loops. . . . . . . . . . . . . . . . . . . . . . . . 60 Math Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 Geometry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 Linear Interpolation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 Modulo Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Randomizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 Trigonometry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 MouseOut versus RollOut . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Pausing/Restarting Your Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Registration Points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 SharedObject . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 Tweening with ActionScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 Variable Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Vector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Chapter 5 Using the Flash API and Starting a Flash Game . . . . . . . . . . . . . 81 Step 1. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 Version 0a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 vii
viii Contents Step 2. . . . . Version 0b . Version 0c. . Version 0d . Version _01. Chapter 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 . 90 . 91 . 93 . 98 Developing a Flash Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Version Version Version Version Version Version Version Version Chapter 7 . . . . . _02. _03. _04. _05. _06. _07. _08. _09. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 . 110 . 115 . 119 . 126 . 135 . 149 . 173 Optimizing Game Performance. . . . . . . . . . . . . . . . . . . . . . . . . 193 Judging and Measuring Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 CPU/GPU Usage and Memory Consumption . . . . . . . . . . . . . . . . . . . . . . . . . 195 Memory Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 Memory Tracking, Memory Use, and Performance Testing . . . . . . . . . . . . . . 199 Optimization Techniques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203 Easiest to Hardest to Implement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204 Greatest to Least Benefit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204 Memory Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 Managing CPU/GPU Usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 Arithmetic Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 Bitmaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 Mouse Interactivity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233 Remove Event Listeners . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235 Stage3D . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235 Type All Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256 Use Vectors Instead of Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258 Chapter 8 Developing and Distributing Games for iOS Devices . . . . . . . . 259 The Tank Combat Game for the iPad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259 Testing an iOS Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308 Publishing Your Game for iOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312 Air for iOS Settings: General Tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 Air for iOS Settings: Deployment Tab . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
Contents Air for iOS Settings: Icons Tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330 Air for iOS Settings: Languages Tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330 Distributing Your Game for iOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331 Chapter 9 Developing and Distributing Games for Android Devices. . . . . 337 Switcher for Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 Testing an Android Game. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367 AIR Debug Launcher. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368 Android Emulators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368 Android Debug Bridge (ADB) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371 Adobe AIR Developer Tool (ADT) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372 Publishing Your Game for Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374 AIR for Android Settings: General Tab . . . . . . . . . . . . . . . . . . . . . . . . . . 374 AIR for Android Settings: Deployment Tab. . . . . . . . . . . . . . . . . . . . . . . 377 AIR for Android Settings: Icons Tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380 AIR for Android Settings: Permissions Tab . . . . . . . . . . . . . . . . . . . . . . . 380 AIR for Android Settings: Languages Tab . . . . . . . . . . . . . . . . . . . . . . . . 381 Distributing Your Game for Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382 Chapter 10 3D Game Development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383 Flare3D. . . Version 01 Version 02 Version 03 Version 04 Version 05 Chapter 11 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384 . 387 . 393 . 401 . 411 . 421 Social Gaming: Social Networks . . . . . . . . . . . . . . . . . . . . . . . . 437 Facebook . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438 Facebook JavaScript API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 444 Adobe’s Facebook ActionScript API . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478 Twitter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482 Adding Twitter Buttons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 484 Requests That Do Not Require Authentication . . . . . . . . . . . . . . . . . . . . 486 Requests That Require Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . 492 Google+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 508 Adding a +1 Button . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509 Adding a Badge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509 Adding a Share Button. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510 Google+ Plug-In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510 Google+ API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 513 ix
x Contents Chapter 12 Social Gaming: Multiplayer Games . . . . . . . . . . . . . . . . . . . . . . 553 Server-Based Multiplayer Games. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 553 Peer-to-Peer Games . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554 Appendix A Errors That Trigger an Error Message . . . . . . . . . . . . . . . . . . . . 575 Appendix B Errors That Do Not Trigger Error Messages . . . . . . . . . . . . . . . 587 Code That Doesn’t Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . Errors Caused by Asynchronous Code Execution . . . . . . . . . . . gotoAndPlay and gotoAndStop (to a Frame Not Yet Loaded) Lost Object References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Repeatedly Executed Code . . . . . . . . . . . . . . . . . . . . . . . . . . . Problems Related to Class File Use . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 587 . 590 . 591 . 592 . 594 . 595 Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 597
Introduction I have been helping solve ActionScript problems for more than 10 years on the Adobe ActionScript forums. During that time, I have addressed tens of thousands of ActionScript issues encountered by forum posters while coding their applications and games. So, this book was written from that perspective and covers everything needed to solve the problems and errors I’ve addressed on the Adobe forums. And, while I surely have not addressed every problem and error that can occur, based on the observation that I rarely see a new problem or error on the forums, I think it’s safe to say that this book covers almost every problem and error that can occur when using ActionScript. What You’ll Find in This Book This book is ostensibly about using Flash Pro and ActionScript 3.0 to create games. And, while this book is focused on Flash game development, most of it also applies to non-game Flash applications. So, this book includes everything needed to solve every ActionScript problem and error you’re likely to encounter. Much of that problem-solving information is in the first 16 or so pages of this book: Chapter 1. I believe it is the most important chapter in this book and I recommend that you do not bypass it. xi
xii Introduction Who This Book Is For This book is intended for developers who already know the basics of ActionScript 3.0 and are at least intermediate-level coders. For readers who need an introduction to ActionScript 3.0, I recommend Trevor McCauley’s tutorial: www.senocular.com/flash/tutorials/as3withflashcs3/ Readers new to ActionScript 3.0 may also find the following links helpful: www.adobe.com/devnet/actionscript/learning/as3-fundamentals/syntax.html www.adobe.com/devnet/actionscript/learning.html www.adobe.com/devnet/actionscript/learning/oop-concepts/writing-classes.html How This Book Is Organized Following is a chapter-by-chapter synopsis of what is covered in this book. n Chapter 1 covers how to debug and test ActionScript coding. n Chapter 2 discusses scope, timeline vs. class coding, and why class coding prevents a major problem introduced by timeline coding. n Chapter 3 shows how to write class code. n Chapter 4 covers topics that ActionScript programmers need to know before starting to program games. n Chapter 5 covers how to find information about everything not covered in the previous chapter. n Chapter 6 shows one way to start developing a game and how to solve problems that typically occur while developing a game using the information from Chapters 4 and 5. n Chapter 7 discusses optimizing game performance. n Chapter 8 covers, in detail, how to develop and distribute games for iOS devices. n Chapter 9 covers, in detail, how to develop and distribute games for Android devices. n Chapter 10 shows one way to start 3D game development. n Chapter 11 covers social gaming using the social networks Twitter, Facebook, and Google+. n Chapter 12 shows how to develop multiplayer games using peer-to-peer communication.
Introduction Companion Website Downloads This book has a companion website offering content related to this book. You can download files from www.cengageptr.com/downloads. Simply enter this book’s title, ISBN, or author’s name in the Companion Search field at the top and click on the Search button. You’ll be taken to the book’s companion page, where you can download the related files. The companion page for this book contains almost all the code that appears in this book. That code is distributed among a number of files you should download from the companion page. The file directories and names that correspond to the code within this book are mentioned in the book’s text. Because this book’s page width constrains the length of text lines, one line of code in this book may be broken into two or more lines of printed text, which can make that line of code difficult to read. However, it is likely that the width of your preferred ActionScript editor allows most lines of code to be displayed without line breaks, which makes the code in the downloaded files much easier to read than the code printed in this book. xiii
This page intentionally left blank
Chapter 1 Problematic Code: Debugging and Testing I don’t know whether anyone can write errorless code on the first attempt, but I know I usually don’t. Based on my experience, I believe you’ll certainly need to debug errors in your code as soon as you start writing it. (In other words, I make many errors, and I assume everyone else does, too.) Learning how to debug your code is crucial to all programming—not just for Flash, not just for ActionScript 3.0, and not just for complex Flash applications such as games. This chapter will show you how to set up Flash to facilitate debugging, how to deal with Flash-triggered error messages, and how to debug errors that don’t trigger error messages. Debugging Tools Efficient code debugging requires two things: deductive reasoning (which I won’t explicitly address) and tools (which I will cover in detail) that let you check your project state at any line of code. The only two tools you need to check your Flash game’s state and debug ActionScript are the trace() function and, when you cannot use that (for example, when you’re testing on a mobile platform), a textfield. For the remainder of this book, if I mention using the trace() function and its output isn’t available to you because of your test environment, use a temporary (debugging) textfield. 1
2 Chapter 1 n Problematic Code: Debugging and Testing You could use a debugging textfield in all situations, but using the trace() function is easier, and it’s faster to set up. Therefore, when trace() function output is available, most coders prefer to use it. Just remember to remove your trace() functions, check “Omit trace statements” in the Publish Settings, or comment out your trace() functions when your game is complete and ready for final deployment. If you fail to do that, then those of us with debug Flash Player versions will have our log files inundated with your trace() function output. Moreover, that output will impair your game’s performance. To debug server-based web projects, I still use the trace() function. To see trace() function output in my browser (and much more), I use the Firefox browser (www. mozilla.org/en-US/firefox/fx/) with the Firebug (getfirebug.com) and FlashFirebug (www.o-minds.com/products/Flashfirebug) add-ons. trace() function output is in the Firebug panel > Flash tab > Output tab. You will need to use a debug version of the Flash Player (www.adobe.com/support/ flashplayer/downloads.html) for web-project debugging. If you’re doing any online debugging, these tools are an immense help. If you deploy your project online, you will probably be doing online debugging. If you deploy more than a few projects online, you will be doing online debugging. Permit Debugging Although those are the only debugging tools needed, if you’re using one of the Flash Pro development environments (CS3, CS4, CS5, CS5.5, CS6, and more will follow), you can speed debugging by using the Flash error messages to pinpoint errors. To pinpoint the exact location of errors, you should enable “Permit debugging” (File > Publish Settings > Flash, check the “Permit debugging” check box, and then click OK). See Figure 1.1.
Permit Debugging Check Permit debugging Click OK Figure 1.1 To enable detailed error messages, check “Permit debugging” and then click OK. Source: Adobe Systems Incorporated. By checking “Permit debugging,” you will see more detailed/helpful error messages. The exact contents of the error messages will depend on the error type (compile-time or run-time), the exact error, and the location (timeline or class file) of your problematic code. But in all scenarios (except for errors that occur during an asynchronous event, such as file loading), the error message will indicate the exact line number of the problematic code—if you check “Permit debugging.” Flash checks for all compile-time errors before checking for any run-time errors (which makes sense). Compile-time errors present as soon as your test movie panel 3
4 Chapter 1 n Problematic Code: Debugging and Testing opens and before any code executes. Run-time errors can occur at any time while your project is running and display when the Flash Player tries to execute problematic code. Compile-Time Errors Compile-time errors will appear in your Compiler Errors panel (see Figure 1.2). Figure 1.2 Compile-time errors appear in your Compiler Errors panel and prevent all code from executing. Source: Adobe Systems Incorporated. Compile-time errors prevent all code from executing, no matter where it is located. You’ll see your main timeline play (along with any MovieClips) from start to finish and then loop. It usually looks like a headache-inducing mess. If you’re prone to seizures, click the Close button on your test movie panel as soon as possible. You don’t need to read the error messages before closing the test movie panel. The error messages will remain in the Compiler Errors panel, where you can read at your leisure so you don’t suffer a migraine or grand mal seizure. With compile-time errors, you can double-click the error message in the Compiler Errors panel, and Flash will display the highlighted line of problematic code in the Actions panel, regardless of whether the problematic line of code is attached to a timeline or in a class file. If your class file isn’t open, Flash will open it (Figure 1.3). You can also read the exact location of the error under the Location header. If the problematic line of code is attached to a MovieClip timeline, this will list the offending MovieClip’s symbol name (the library name), layer name, frame number, and line number.
Permit Debugging Figure 1.3 Compile-time error in a class file. Source: Adobe Systems Incorporated. However, if your error is on the main timeline, instead of the symbol name you’ll see the scene name or document name (if you have a document class). (Refer to Figure 1.2.) It’s a good idea to learn how to read the error’s location, because double-clicking doesn’t always work correctly. If the highlighted line doesn’t match the line number listed in the error message, use the line number in the error message. It is always correct. After you correct all compile-time errors and retest, you will see run-time errors, if there are any. Run-Time Errors With run-time errors (displayed in the Output panel, not the Compiler Errors panel), double-clicking the error message doesn’t do anything useful. You must read the error message. (See Figure 1.4.) Figure 1.4 Run-time error on the main timeline. Source: Adobe Systems Incorporated. 5
6 Chapter 1 n Problematic Code: Debugging and Testing With file loading (asynchronous) errors, the error message will have only one line. Even if you checked “Permit debugging,” there won’t be any information to help you pinpoint the load method that triggered the error. Fortunately, because that error message will occur shortly after your problematic load method, it is usually easy to determine which (if you have more than one) load method triggered the error. If you have a number of load methods and there’s no quick and easy way to determine which load method triggered the error, you can use the trace() function. Because run-time errors stop code execution only in the scope (defined in Chapter 2, “Avoiding Problematic Code”) of the problematic code and stop only the code that runs after the error, trace() function output just before the problematic line of code will work, and trace() function output just after the problematic line of code will fail. Using that information will always allow you to pinpoint the line of code that triggers a load error. For all other run-time errors, the error message will pinpoint the exact location of the error. The error message will have at least two lines, and often there will be many more that display the entire sequence of function calls that led to the problematic line of code trying to execute (which is the always in the second line of the error message). See Figure 1.5. Figure 1.5 Run-time error on the main timeline showing the sequence of function calls that triggered the problematic line of code. Source: Adobe Systems Incorporated. You’ll rarely need to check more than the first two lines in the error message. The first line lists the error number with a brief explanation of the error, and the second line lists the exact location of the problematic line of code. I’ll discuss the first line of the error message in the upcoming “Errors That Trigger Error Messages” section. The second line will look something like at you_can_ignore_this_part[good_stuff_here].
Permit Debugging The bracketed content has all the information you need to pinpoint the run-time error. If the error is on a FLA file’s main timeline, you will see in the brackets the SWF file’s name_fla.MainTimeline::frame number:line number. For example, the error displayed in Figure 1.5 reveals a 1034 error on the main timeline of the FLA that published Untitled.swf at Frame 1, Line 17. The following is an aside that may prove helpful when you’re testing files. Flash doesn’t handle the dash in a SWF name correctly. That seems strange, because Flash uses a dash in FLA names by default, and by default the SWF name will match and have a dash. Anyway, the SWF name displayed in run-time error messages reflects this problem with dashes. I was testing Untitled-1.fla, which published Untitled-1.swf, but Figure 1.5 displays an error message that implies a problem with Untitled.swf. That may or may not interest you, but it’s not a significant problem to deal with. However, if you use something like Untitled-1.swf to load Untitled-2.swf, you will have all sorts of problems because Flash doesn’t appear to distinguish those two SWFs. It seems as if they both look like Untitled.swf to Flash, causing your loading SWF to try to load itself instead of Untitled-2.swf. Until you realize that the problem is Flash, you might waste time trying to debug errorless code. Other than the problem with dashes that prematurely terminates the SWF’s file name, the SWF’s full name will be in the run-time error message. If the error is on any timeline other than the main timeline, in the brackets you will see the SWF’s name_fla.symbol name_error number::frame number:line number. For an example, see Figure 1.6. Figure 1.6 A 1034 error on the timeline of Symbol1 at Frame 1, Line 19. Source: Adobe Systems Incorporated. 7
8 Chapter 1 n Problematic Code: Debugging and Testing Here the symbol name is the library symbol, not the instance or reference name. Look in your Library panel to find that symbol. White space in the symbol name is removed for display in the error message. The _1 appended to MovieClip symbol Symbol1 means Symbol1 is the first MovieClip symbol with a run-time error. If there are other MovieClip symbols with run-time errors, an underscore and increasing whole numbers are appended to the symbol name. I’ve never found that factoid particularly helpful, but I think too much information is better than too little (at least as it applies to debugging). If the error is in a class file, in the brackets you will see the class file path/name:problematic line number. For an example, see Figure 1.7. Figure 1.7 A 1034 error on Line 13 in the class file MC.as in the F:\Flash\_test files\ directory. Source: Adobe Systems Incorporated. You might think the SWF file name would be a superfluous bit of information, too. But the culprit SWF with the run-time error may not be your main SWF. If you load a SWF that has a run-time error, you will find that the SWF name in the error message is helpful. (A SWF that would contain a compile-time error if Flash were to publish the SWF and add the problematic code contains no code, so you cannot load a SWF that has a compile-time error.) Custom Profile Instead of doing the same File > Publish Settings > and so on clicking and checking each time you open a new FLA file, you can create a custom profile that checks that option, and any other options you typically use, by default. Unfortunately, you still have to import a custom profile each time you open a new FLA, which requires almost as many clicks as selecting your desired options each time you open a new FLA.
Custom Profile To create a new default profile that will select your desired options each time you open a FLA without requiring any clicks, do the following after checking “Permit debugging”: 1. Click OK to close the ActionScript Settings panel. 2. Click the gear icon (Profile Options) at the upper left of the Publish Settings panel. (Click File > Publish Settings if your Publish Settings panel is closed.) 3. Click Export Profile and give your exported profile a name other than default. xml. 4. Save it to the default directory suggested by Flash. 5. Open a file browser and copy your saved profile from the default directory (you can check that location by clicking the gear icon and inspecting the path, or you can search for your saved profile) to the subdirectory en_US/First Run/Publish Profiles in your Flash Pro’s install directory. If your install language isn’t en_US, that first subdirectory will have a different (but easy-to-recognize) name. 6. In both the Publish Profiles subdirectory and the default subdirectory, rename default.xml to the name you generally use to save original files (such as default_BU.xml, default_ORIG.xml, $default.xml, or whatever convention you use). 7. Copy your custom-named exported profile to the Publish Profiles subdirectory. 8. Rename the copied exported profile in the Publish Profiles subdirectory to default.xml. 9. Copy default.xml from the Publish Profiles subdirectory to the default directory. 10. Copy your custom-named profile from the default directory to the Publish Profiles subdirectory. You should have three files in each directory. (I don’t see why there’s a need for all of those files, but deleting any other than the backups may cause Flash to revert to its original default settings. You may not find that happening until you restart your computer, so be careful if you’re testing which files you can delete.) After you set up that custom profile, you will start with “Permit debugging” checked when you open Flash Pro. You can save other preferred settings as the default for all of your FLA documents in the same way. If you work on only a few projects per year, it may not be worth that 10-step effort to create a custom profile. But even if you work on only one Flash project, it will be worthwhile to enable “Permit debugging.” 9
10 Chapter 1 n Problematic Code: Debugging and Testing Errors That Trigger Error Messages You might find that some error messages need no explanation (such as Compiler Error 1021: Duplicate Function Definition). But if you need further explanation, check the Flash help file Appendixes (Help > Flash Help > ActionScript 3.0 and Components > ActionScript 3.0 Reference for the Adobe Flash Platform > Appendixes), where you’ll find a complete listing of all compiler and run-time error messages, often with additional and helpful information. As of this writing, that link is http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/appendixes.html, but that may change as Adobe publishes updated help files. This is the first place you should check when you encounter an error message that you don’t completely understand. The additional information may be enough to save you hours of hair-pulling frustration. For example, if Error 1021: Duplicate Function Definition isn’t clear enough for you to understand the problem, the additional, “You cannot declare more than one function with the same identifier name within the same scope,” may be enough to help you resolve the issue. But if that’s not enough help, don’t be afraid to use a search engine. Searching for “Flash as3 error xxxx” should bring up all sorts of information, some of which may be helpful. Further, this book’s Appendix A, “Errors That Trigger an Error Message” lists the most common Flash error messages encountered by people posting on the Adobe forums, along with advice on how to solve them. The error messages covered in Appendix A (a small subset of the errors listed in the Flash help appendixes) are discussed in much greater detail than in the Flash help files. Appendix A encapsulates some of my experience helping on the Adobe Flash forums (http://forums.adobe.com/community/flash/flash_actionscript?view=discussions&start= 0&numResults=50, http://forums.adobe.com/community/flash/flash_actionscript3?view= discussions&start=0&numResults=50, http://forums.adobe.com/community/flash/flash_ general?view=discussions&start=0), answering many thousands of queries, plus my experience with the errors I’ve made coding Flash projects. If you’re in no rush for an answer, there is also help available on the Adobe Flash ActionScript 3 forum (http://forums.adobe.com/community/flash/flash_action script3?view=discussions&start=0&numResults=50). You should use whatever combination of help resources works best for you.
Errors That Do Not Trigger Error Messages Errors That Do Not Trigger Error Messages Most ActionScript programming errors trigger a Flash error message—either a compile-time error or a run-time error. However, some errors don’t trigger a Flash message. These are the result of some combination of faulty logic and a failure to understand how Flash/ActionScript works. I’ll discuss the more common of these errors in Appendix B, “Errors That Do Not Trigger an Error Message.” But whether you encounter a common error or an uncommon one, you can still debug it using the trace() function. Generally, your code will work up to a certain point and then fail. That failure point is where you should start using the trace() function to debug your error. For example, in the following code, I create two red circles, place them on-stage, and assign MouseEvent.CLICK listeners. The problem is that the leftmost red circle doesn’t respond to the mouse. (See Figure 1.8.) var mc:MovieClip; mc=new MovieClip(); with(mc.graphics){ beginFill(0xaa0000); drawCircle(0,0,20); endFill() } addChild(mc); mc.x=100; mc.y=100; mc=new MovieClip(); // this is where the problem starts with(mc.graphics){ beginFill(0xaa0000); drawCircle(0,0,20); endFill() } addChild(mc); mc.x=200; mc.y=100; mc.addEventListener(MouseEvent.CLICK,f); mc.addEventListener(MouseEvent.CLICK,f); function f(e:MouseEvent):void{ trace(e.currentTarget.x); } 11
12 Chapter 1 n Problematic Code: Debugging and Testing Does not respond to the mouse Does respond to the mouse Figure 1.8 The on-stage display after executing the code. Source: Adobe Systems Incorporated. This code works (both circles are created and added to the display in the expected locations) up until the leftmost red circle (supposedly) has its listener added. To check whether the leftmost circle has a MouseEvent listener added, you need to use some code that distinguishes the two MovieClips. Because the x property of each circle is the only other feature that easily distinguishes the two circles, that is what we will use. Starting at the second mc.addEventListener(MouseEvent.CLICK,f); and using trace(mc.x); you will see 200 in the Output panel, revealing that we have no code adding an event listener to the leftmost circle. If you keep moving that trace(mc.x) line closer and closer to the first line of code, you’ll pinpoint the exact line where the problem is introduced. Above that second mc=new MovieClip(); outputs the first circle’s x property. Below that line of code, it does not. The only logical conclusion: The second mc=new MovieClip() is causing us to lose our reference to the first circle. trace(mc.x)
Errors That Do Not Trigger Error Messages A reference is a value that points to a datum. In this situation, the value is mc, and there are two data: the leftmost and rightmost MovieClips. Trying to use one reference to point to two MovieClips is problematic. Now, why would that happen? We have mc referencing our first circle, and then as soon as we assign mc to a new MovieClip, we lose our reference to the first circle. Conclusion: One variable (such as mc) cannot reference two different objects at the same time. Flash reassigns that one mc variable to point to the last assigned object. One way to remedy this is to assign that listener to leftmost circle. mc while it still references the var mc:MovieClip; mc=new MovieClip(); with(mc.graphics){ beginFill(0xaa0000); drawCircle(0,0,20); endFill() } addChild(mc); mc.x=100; mc.y=100; mc.addEventListener(MouseEvent.CLICK,f); // mc is still the first created circle mc=new MovieClip(); // mc is no longer the circle above. with(mc.graphics){ beginFill(0xaa0000); drawCircle(0,0,20); endFill() } addChild(mc); mc.x=200; mc.y=100; mc.addEventListener(MouseEvent.CLICK,f); // this mc is the second created circle function f(e:MouseEvent):void{ trace(e.currentTarget.x); } 13
14 Chapter 1 n Problematic Code: Debugging and Testing If that appears to be an error you think no one would make, here is exactly the same error in slightly different guise that someone posted on the ActionScript 3 Adobe forum while I was writing the first draft of this page: for (var io:int = 0; io < 4; io++) { var opBtn:Btn_operator = new Btn_operator(); // there’s more code here irrelevant to this discussion opBtn.addEventListener(MouseEvent.CLICK, pressOperator); } And then later in his code, the poster wanted to disable his wrote, “I tried: opBtn instances and opBtn.mouseEnabled=false; but it didn’t seem to work.” This is exactly the same error as my first example, but with the duplicate variables “hidden” in plain sight in a for loop, which is the way this error typically occurs. You create an object reference that is repeatedly used so the most recent object reference overwrites the previous object reference. At the end of the for loop, that reference refers to only the last created object. Appendix B will cover this issue in more detail. The point is that logical use of the trace() function allows you to pinpoint exactly where your code fails. Once you know exactly where your code fails, you should be able to determine why it fails and then determine what needs to be done to correct it. Testing and Experimenting Unless you already know everything discussed here, it will be difficult to read this book (or any other coding book) from start to finish and retain that information. I encourage you to read a little, think a little, question everything, write some code, and test your code to see the results. Even when you’re in the midst of a major project, you can always save your files (you don’t need to close them), open a new test FLA, and test code snippets without causing any problems with your main project. If you encounter or anticipate a problem or you aren’t sure how to use some property, method, or anything else, opening a test FLA can save you time, prevent debugging problems in your main project, and show you how Flash will handle various situations.
Testing and Experimenting For example, if you’re not sure whether new sounds can play after executing SoundMixer.stopAll(); you can test it. This is so important that I’m going to say it again: If you’re reading something in this book (or any ActionScript book) and you question how something works, stop, open Flash (if it isn’t open already), write code, and test. If Flash is already open, save your work, write code, and test. You can’t do any major harm by testing, and if you’re anything like me, you’ll learn coding more thoroughly and easily if you read a little, absorb some info, set aside your reading material, write some code, and test your understanding of what you just read. If you think of issues or situations not addressed in your reading material, write the appropriate code and test how Flash handles it. As I mentioned, you cannot cause any major harm by testing. The worst that can happen is that Flash may crash. That’s a problem if you also have files with unsaved work open, but if you save first, even that is no great disaster. While we’re discussing saving your work, this might be a good time to remind you to save your work frequently. Even less obvious but just as important: Every so often, save your FLA file with a new name so that if one version gets corrupted, it won’t take long to re-create your latest FLA. Using a consistent naming convention (such as appending 01, 02, and so on to your file name) is a particularly good idea. For example, if you start working on spacegame.fla, your first save should be spacegame01.fla or spacegame_01.fla. When you finish a project, you can delete the old versions or move them to a subdirectory to keep your directories tidy. Do not delete all your backup files until you have closed Flash, shut down your computer, restarted your computer, restarted Flash, and successfully reopened your files. Once you know that you can reopen a FLA after a complete shutdown, you can be confident that the just-opened version isn’t corrupted, and you can delete all the backups. But don’t save over that last version. Again, save with a new file name if you don’t have any backups. In general, the more slowly you delete backups, the better. And the fewer backups you delete, the better. You can create backups to files other than your FLA files, but corruption of class files is extremely unlikely (because class files are plain text files, not binary files), so it’s not as important to back them up. However, if you’re about to make some major 15
16 Chapter 1 n Problematic Code: Debugging and Testing changes to a class file and you want to maintain a copy of the previously working version, save it using a naming convention that works for you. I save class file backups using the same convention that I use for FLA files (by appending _xx). I actually hadn’t done that very often in the past, but while writing this book, I found I needed to create multiple versions of class files to show you how I work. In later chapters you’ll see an explanation of this naming convention as it applies to class files and how to use these (pseudo) class files. In case you start doing this before reading those later chapters, the class files used in your project will not include the appended_xx. So, for example, if you have a class named Main.as and you want to save evolving versions of this class, you would save Main_01.as, Main_02.as, etc. When you want to test one of those versions, you would rename them Main.as. Final Words on Debugging Do yourself a favor: Format your code so you have indents and spacing that make your code legible. Trying to debug unformatted code is like trying to sum a misaligned column of 25 numbers with pen and paper. It makes the task more difficult and more time-consuming for no good reason and with no compensatory benefit. Flash even has an auto-formatting option if you’re daring. But be careful if you use auto-formatting. It can occasionally convert error-free code into problematic code. You can always use the Undo command (Edit > Undo) to undo any problems caused by auto-formatting, but for the most part, I find it easier to maintain good formatting without using auto-format and risking the occasional problems. When you use auto-format on code that contains more than 100 lines, it’s easy to overlook an auto-format-induced problem. So, if I have a block of 20 or 30 lines of code that need to be formatted in the midst of many more lines of code, I may open a new FLA, paste the code in the Actions panel, and use auto-format. If the code looks good, I’ll paste the formatted code over the unformatted code and then test to make sure no errors were introduced in the formatting process. Finally, if you see more than one error message, correct the first error and then retest. That may sound like worthless advice, but often subsequent error messages will resolve after you correct the first error.
Chapter 2 Avoiding Problematic Code The previous chapter was about debugging code, and this chapter is about writing code that minimizes errors that need to be debugged. Failure to understand scope as it relates to Flash is a major cause of errors (which trigger error messages). In fact, I believe it’s a certainty that failure to understand scope will lead to coding errors. You could skip this chapter, generate errors, debug, and gradually learn about scope. But reading this chapter will save you time. This chapter covers all you need to know about Flash timeline scope, class scope, and function scope, thereby helping you avoid most scope-related coding errors. Before I start the discussion of scope, I want to discourage timeline coding. If you avoid timeline coding, you will avoid a number of coding pitfalls and speed your progress through this chapter. Timeline Coding versus Class File Coding There are some basic differences between timeline coding and coding in class files (class coding, for short). There is no requirement that you do all of one or all of the other. You can combine timeline and class coding in any Flash project. But, in general, you should limit your timeline coding to little more than a stop(), gotoAndPlay(), or gotoAndStop(). I will refer to any more ActionScript than one of those three methods attached to a timeline as significant timeline coding. For elementary projects (for example, 100 or fewer lines of code), significant timeline coding, using one frame in one timeline to attach all your code, is acceptable. It’s not necessarily good coding practice, but it’s acceptable in those circumstances. 17
18 Chapter 2 n Avoiding Problematic Code For debugging and testing, you can add code anywhere you need for convenience. Debugging and testing code will not need editing, extending, or maintaining—all drawbacks of significant timeline coding. For anything more complex, such as all but the simplest games, you should use class files for your coding. It may seem more difficult and time-consuming to set up a project using class files, but it’s much easier to debug, prevent bugs, and maintain and extend a game when you’re using class files. If you’ve ever added as little as 1,000 or 2,000 lines of code to a timeline, you’ve probably encountered an Actions panel delay. It takes some versions of Flash Pro a few seconds (or more) to display all the code that you’re likely to create when coding a complex game, if that code is on a timeline. It takes even longer to scroll through all that code looking for a section you want to edit. Spreading code across more than one frame of a timeline and especially in more than one timeline is a time-wasting mess and is strongly discouraged. Anyone who has ever worked on a Flash file with significant bits of code spread over more than one timeline understands the incredible amount of time that can be wasted looking for pertinent code more than a few weeks after it was created. The worst offenders I’ve seen (and I’ve seen hundreds of problematic Flash projects) in the highly competitive worst Flash coding derby are templates sold by websites such as www.entheosweb.com. Don’t be fooled into thinking you can get a headstart on a project by starting with a web template offered for sale. There may be some well-coded templates sold online, but I’ve never seen one. At a minimum, if you may one day want to edit a template, you should ask the vendor whether there is any significant timeline coding. If the answer is yes, do yourself a favor and look elsewhere, or save your time and money and make your own. Scope ActionScript is an object-oriented programming (OOP) language. OOP languages use objects to prevent data from being global. Each line of code in an OOP language is within some scope. That is, data is confined to some object. In Flash, those objects are MovieClips or classes. If you define something (for example, a variable) on a MovieClip’s timeline, the scope of that variable is the MovieClip, and that variable is bound to the MovieClip. You can only access that variable directly by using code on that MovieClip’s timeline or by using code that references both that MovieClip and the variable.
Scope If you define a variable in a class, that variable’s scope is the class. Again, that variable is bound to that class. You can directly access that variable only by using code in that class or by using code that references that class and that variable, or an instance of that class and that variable. One significant benefit of this is that something defined in one scope won’t conflict with anything outside of that scope. That’s why you can define an init (or any other) function in each of your DisplayObject classes and be confident that it won’t conflict with any other, even though their names are identical. (If you had two identically named functions in the same scope, the compiler would generate a 1021 Duplicate Function error.) One significant drawback of scope is that something defined in one scope isn’t readily available in another scope. This is essentially another way to restate the previous paragraph’s first sentence; the most important benefit is the same as the biggest drawback. How you view scope (as a frustrating drawback or a beneficial feature) depends on how well you understand it. I don’t think scope and OOP are necessarily intuitive, but once you understand scope, its benefits over sequential programming, especially for complex applications such as games, become apparent. MovieClip Scope If you heed my earlier admonition about avoiding significant timeline coding and you don’t have to edit anyone else’s poorly coded projects that contain significant timeline coding, you can skip this subsection and advance to the “Class Scope” subsection. No code in this book will add significant code to timelines, so you don’t need to understand MovieClip scope to read this book. However, if, like me, you’re in the unfortunate category of souls who have to deal with other coders’ problem projects, then I’m sorry for you, and I’m going to cover everything you need to know about MovieClip scope. (And then I’ll never mention it again in this book.) When using timeline coding, you reference an object on that timeline using dot notation. For example, if you have a MovieClip on the main timeline stage with instance name mc1 and you define a function on mc1’s timeline, like this: function mc1F():void{ trace(this.name); } 19
20 Chapter 2 n Avoiding Problematic Code You would call mcF from the main timeline using: mc1.mc1F(); For on-stage objects, if you fail to assign an instance name (by clicking the object to select it and assigning a name property in the Properties panel), you won’t have an easy way to reference it or any of its properties (variables) or methods (functions). That is, there will be no easy way to call mc1F() from outside of mc1’s scope (or timeline). Notice that trace(this) will ouput the scope that contains the Sometimes that can be helpful. trace() function. You can chain MovieClip references using any number of MovieClips to reference an object in one timeline from any other timeline. If you develop the (bad) habit of coding on many different timelines, you can end up with complex path (the reference from one timeline to another) problems. Path problems are one reason (albeit a minor one) why you should limit the code you place on different timelines. For example, you could end up with something like the following if a function named were defined on a MovieClip timeline ggchild1 that was a child of gchild1 that was a child of child1 that was a child of parent1: f1() parent1.child1.gchild1.ggchild1.f1(); And if you wanted to call a function named parent1F() defined on the line from the ggchild1 timeline, you would use: parent1 time- parent.parent.parent.parent1F(); This is awkward, and you should avoid this type of coding by minimizing code placed on more than one timeline. In addition, for complex projects such as games, it will be easier to code using class files. But if you do find yourself in a situation where you need to reference one timeline object from another timeline and you don’t know the path, you can use the trace() function to reveal the correct path. In fact, you can reveal the path to any MovieClip’s timeline: timelinePathF(); function timelinePathF():void { if (this.name.indexOf(“root“)>-1) { trace(“MovieClip(root)“); } else { var mcBool:Boolean; MovieClip by adding this code to that
Scope var s:String=this.name; var mc:MovieClip=MovieClip(this.parent); while (mc.name!=“root1“) { if(mc.parent is MovieClip){ mcBool = true; s=mc.name+”.”+s; mc=MovieClip(mc.parent); } else if(mc.parent is Loader){ mcBool = false; break; } } if(mcBool){ s=“MovieClip(root).”+s; } else { s = “MovieClip(getChildByName(“+mc.parent.name+“).content).”+s; } trace(s); } } If you add the same code to a second MovieClip’s timeline, you can see how to reference an object that is defined on one timeline using code that is on the other MovieClip’s timeline. For example, if you have a function f() defined on the timeline of a MovieClip mc_a6 with timeline path: MovieClip(root).mc_a1.mc_a2.mc_a3.mc_a4.mc_a5.mc_a6 and you want to reference path: f() from the timeline of a MovieClip mc_b2 with timeline MovieClip(root).mc_a1.mc_a2.mc_b1.mc_b2 count back to the first common ancestor (mc_a2) of the two MovieClips and then advance to the timeline with f(). Therefore, from the mc_b2 timeline, this references the mc_b2 MovieClip, this.parent references the mc_b1 MovieClip, this.parent.parent references the mc_a2 MovieClip, and going forward to f() yields: this.parent.parent.mc_a3.mc_a4.mc_a5.mc_a6.f(); which is one way to reference the function f() in mc_a6 from mc_b2. This is an example of a relative path. That is, the path is relative to both mc_a6 and There is always a relative path because two MovieClips always share at least one common ancestor, MovieClip(root). mc_b2. 21
22 Chapter 2 n Avoiding Problematic Code You can also use an absolute path—a path that is independent of the code’s location. An absolute path starts with the main timeline (root) and then uses dot notation to indicate the MovieClip of interest. An example of an absolute path is: MovieClip(root).mc_a1.mc_a2.mc_a3.mc_a4.mc_a5.mc_a6 You can place that code on any timeline anywhere in the same SWF that contains those MovieClips, and it will correctly reference the MovieClip mc_a6 (as long as it exists when the code referencing it executes). An absolute path is easier to determine than a relative path, but there is a major problem with absolute paths: The absolute path will change if the SWF containing it is loaded into another SWF. That is a major problem because when you’re creating more complex games and completing the work on one SWF that will be loaded into another, you’ll find that the completed SWF needs more work to adjust all absolute paths. And, once you fix that absolute path so the loaded SWF works correctly when loaded, it won’t work correctly if tested as a stand-alone SWF (in other words, without being loaded). Those path problems are expected when using absolute paths and are something you’ll probably contend with if you edit projects with significant timeline coding. You can use timelinePathF() to determine the correct path even when the SWF containing that function is loaded into another SWF. I’m sure there are situations where timelinePathF() will fail to reveal the correct path, but by using the trace() function you should be able to extend timelinePathF() to work for any situation. Whether you’re using an absolute or a relative path, MovieClip mc_a6 has to exist when code trying to reference it executes. If f() is an anonymous function, not only must the MovieClip mc_a6 exist, but the frame containing the definition of f() must also play before the frame that contains code that tries to reference f(). Because named functions are compiled and ready for use before the frame that contains the function executes (and even if it never executes), the previous sentence doesn’t apply to named functions. An anonymous function has the form: var f:Function = function():void{ ... }
Scope A named function has the form: function f():void{ ... } Further, if you’re trying to reference a variable or an object such as a MovieClip, the frame containing it must play before you can reference the variable or object. Trying to reference an on-stage object before the object exists (in other words, before the first frame containing that object plays) is a much more common error than faulty function references, and it requires more work to remedy. You’ll typically encounter this when code on one frame uses gotoAndPlay() or gotoAndStop(), and an object in the target frame needs to be referenced using code on the frame that contains the goto function. The usual remedy for that is to add an Event.RENDER listener and invalidate the stage. For example, if you have a textfield tf that exists only on Frame 3, the following code, on any other frame of the same timeline, will be able to reference tf. informs the flash player to dispatch an Event.RENDER event to each display object that has an Event.RENDER listener the next time the player renders the display. stage.invalidate() stage.invalidate(); var cFrame:int = currentFrame; addEventListener(Event.RENDER,f); gotoAndStop(3); function f(e:Event):void{ tf.text = “The previous frame was “+cFrame; } Instead of wasting time discussing how to deal with that remedy and the other problems caused by using significant timeline coding, I’ll just tell you to save yourself the aggravation and avoid it. Class Scope It is important to understand class scope because failure to understand it leads to many errors and much frustration. The concept is simple, but understanding it seems difficult for many people. There isn’t much to explain, so read (and possibly reread) the next paragraph carefully. The scope of everything defined in a class file is that class. That is, the scope of every variable and every function defined in a class file is that class. 23
24 Chapter 2 n Avoiding Problematic Code For example, if you define a variable var1 in a class Main, the scope of var1 is Main. That means you cannot reference var1 outside of the class Main except when you explicitly choose to expose it to other scopes (or classes). The next chapter will discuss how you expose variables and functions defined in one class to other classes. Function Scope When you declare a property using the var keyword inside a function body, that property is defined only within that function body and only while the function is executing. That is, the property’s scope is the function. The property expires (in other words, does not exist) when the function completes execution. Even if you immediately recall the same function, the property’s value from the previous call is irretrievable. Failure to understand function scope is the source of many errors. For example: package { public class TestClass { public function TestClass() { // stringVar is local to the TestClass constructor var stringVar:String; = “string 1“; init(); } private function init():void{ // This will trigger an 1120 error because stringVar is undefined // here. trace(stringVar); } } } will generate an error, 1120: Access of Undefined Property stringVar. If you want to define a property within a function body and you want it to be available outside that function, you must declare the property outside of all functions. package { public class TestClass { // stringVar declaration private var stringVar:String; public function TestClass() { // stringVar definition stringVar = “string 1“; init(); }
Scope private function init():void{ trace(stringVar); } } } Function scope is the same whether your function is defined in a class or on a timeline, and it’s the same whether your function is anonymous or named. This is such a common problem that I’m starting to think everyone eventually encounters this issue. So, I think it is worth repeating. If you define a variable to be local to that function (by prefixing with the keyword var), that variable and that variable’s value are only available inside that function while that function is executing. There is a similar issue with functions declared locally. For example, in the following code, f2() is local to f1(). If you try to call f2() outside of the f1() function body, you will trigger an 1180: Call to a Possibly Undefined Method f2 error. function f1():void { // Trying to call f2 from here will trigger a runtime 1006 error: value is not // a function because f2 is not defined, yet. var f2:Function = function(){ }; // This will work because the call is from within the f1 function body. f2(); } // Trying to call f2 from here will trigger an 1180 error no matter when or how many // times you call f1. With anonymous functions, just like with variables, you can declare the nested function outside of the nesting function body if you want to make the nested function available outside of the nesting function. For example, the following won’t trigger any errors: var f2:Function; function f1():void { f2 = function(){ }; } f1(); f2(); 25
26 Chapter 2 n Avoiding Problematic Code However, because you cannot declare a named function, you should never nest a named function like the following: function f1():void{ // You can only reference f2() from here function f2():void{ } // or here - both within f1() } f1(); // But not here. This will trigger an 1180 error. f2(); There is nothing (other than defining f2() outside of f1()) you can add to that code to make f2() available outside of f1(). You should un-nest those functions. Un-nesting those functions has no drawbacks and has two major advantages: You can call f2() from outside of f1(), and your code is easier to read and debug. function f1():void{ } function f2():void{ } Further, there is no benefit to nesting a named function, so you should never do so.
Chapter 3 Writing Class Code By convention, class files usually follow the same general format: 1. Package designation 2. Import statements 3. The class declaration 4. Class-scoped (that is, not local to a function) variable declarations 5. The class constructor 6. Other class functions In this chapter we’ll start writing class code covering these six components, expand on the previous discussion of class scope, and discuss some of the ways that variables, functions, and objects defined in one class can be exposed to and accessed by other classes. Open Flash, start a new ActionScript 3.0 document, and save your FLA into a new directory using a file name more meaningful than the default Untitled-n.fla that Flash offers. Then click on an empty part of the stage (which should be the entire stage at this point) or the pasteboard and, in the Properties panel, enter a document class name (for example, Main). Click the pencil icon (Edit Class Definition), and your default class file should open (see Figure 3.1). Save this with the suggested name (the only one that should be given to this file), Main.as. 27
28 Chapter 3 n Writing Class Code Document class name main entered Edit Class Definition icon Figure 3.1 Properties panel with document selected. Source: Adobe Systems Incorporated. If you have an older Flash Pro CS version, your basic Main class won’t be created for you when you click the pencil icon, and you may see a warning like the one in Figure 3.2. Figure 3.2 Warning displayed after clicking the Edit Class Definition icon in some older Flash Pro versions. Source: Adobe Systems Incorporated.
Writing Class Code In that case, Flash also won’t suggest the correct file name, so copy the following into a new class file (File > New > ActionScript File > OK) and save it as Main.as: package { import flash.display.MovieClip; public class Main extends MovieClip { public function Main() { // constructor code } } } The first line of all AS3 class files contains the keyword package, which indicates which directory (relative to the location of the FLA, not the SWF) contains the class. If a class is in the same directory as the FLA, your package designation should look like the previous code. If a class is in a subdirectory C of the FLA-containing directory, your code should look like this: package C{ ... } And if you wanted to save your class in a subdirectory C2 of the subdirectory C1, your class package would indicate that by using: package C1.C2{ ... } After the package designation, needed classes are imported. You can add class source paths in your Publish Settings, but for simplicity we’ll assume that you have only the default Flash class path. Therefore, you’ll need to import every class that isn’t in the same directory with your FLA and used in Main. Because we are extending the MovieClip class and no other class is used in Main (yet), the MovieClip class is the only one you need to import. Because the MovieClip class is part of the flash.display package (that is, the Adobecreated MovieClip class is in the flash/display subdirectory of the default class path), import flash.display.MovieClip is used. If you know you need to import the MovieClip class, but you don’t know or can’t remember which package it’s in, open the Flash help files, navigate to the ActionScript 3.0 classes, and then navigate to MovieClip. The topmost line will indicate the MovieClip package. The same is true for all other Flash classes. (I will cover this in more detail in Chapter 5, “Using the Flash API and Starting a Flash Game.”) 29
30 Chapter 3 n Writing Class Code The third line of code declares that the class is public and the name is Main, and it indicates that the class extends the MovieClip class. Because Main extends the MovieClip class, you can use all the MovieClip properties, methods, and events (after you import the needed event classes). We’ll cover the public attribute in the next section. The fourth line is called the class constructor. It is a function that executes each time a class member (or instance) is created. Because Main is a document class, the constructor will execute only once each time your application (for example, a SWF) starts. Notice that three names exactly match each other: the class name, the class constructor, and the name of the file. If the class name and file name don’t match, you’ll trigger a Flash 5008 error (see Figure 3.3). Figure 3.3 Error trigged by a class name/file name mismatch. Source: Adobe Systems Incorporated. If the constructor doesn’t match the class name and file name, there won’t be an error. In fact, you don’t even need to define a constructor. If there’s no code that needs to be executed as soon as a class instance is created, there’s no need for a constructor. So, if you mistype a constructor name, Flash has no way of knowing you intended that function to be a constructor and will assume that you created some other function, and you won’t trigger an error message. The only problem you’ll see is that the code in your misnamed constructor won’t execute when a class instance is created. A trace(this) in your misnamed constructor will confirm that the code isn’t executing and should prompt you to search for a constructor name typo.
Internal, Private, Protected, and Public Properties Also, if the package designation and the class file’s location don’t match, you may or may not see an error message. If Flash attempts to compile the class file, you will see a Flash 5001 error (see Figure 3.4). Figure 3.4 Error trigged by having a package/directory location mismatch. Source: Adobe Systems Incorporated. But Flash may not attempt to compile the class file, so no error message will be triggered. For example, if you have a document class of com.Main, and there’s no Main.as in the com directory or there’s no directory named com, Flash won’t find Main.as and won’t attempt to compile it. For coders uncomfortable with OOP, a common source of problems is trying to access objects defined in one class from another class. I’m going to show you some ways you can do that. First, you’ll need to understand method and property attributes. Four attributes can be designated for a method, and the same four can be designated for a property— internal, private, protected, and public. The following section will explain these. Internal, Private, Protected, and Public Properties When using class coding, you can reference any public method (function) or property (variable) using dot notation. You cannot directly reference any property or method that is designated as private unless you’re within the class scope (in other words, your code is in the same class). And there are two other attributes you can assign to class properties and methods: internal and protected. Designating a property or method as internal makes it available to anything within the same package, and assigning a property or method as protected restricts availability to the class and its subclasses (if there are any). internal is the default attribute, so 31
32 Chapter 3 n Writing Class Code if you assign no attribute, the property or method will behave as if it has the internal attribute. That is, it will be available to any other class in the same package. Now, saying a property or method is available to any other class or any class in the same package and so on doesn’t change the fact that ActionScript is an OOP language. That is, the property or method is still defined only in the scope of the class. And while it may be available outside the class, you still have to use the correct (dot notation) code to reference it correctly. So, for example, if in the class C1 you have: C1.as package { // This is the class declaration public class C1 { // The line below is, by default, the same as // internal var var1:String=“from C1”; var var1:String=“from C1”; public function C1() { // This is the class constructor and must match the name of the // file and the class name if you want this code to execute each // time a class instance is created. } } } and you want to reference class Main—you could use: var1 from outside the scope of C1—for example, in the Main.as package { import flash.display.MovieClip; public class Main extends MovieClip { public function Main() { // Create an instance (c1) of C1 var c1:C1 = new C1(); // Because var1 is an internal property you can access it outside // the scope of C1 as long as Main is in the same package as C1 trace(c1.var1); } } } Also, because C1 into Main. C1 is in the same package as Main, you don’t need to explicitly import
Internal, Private, Protected, and Public Properties If C1 isn’t in the same package as Main, you must use the public attribute (not the default internal), and you must explicitly import C1 into Main. Before you try the following code, delete C1.as from the FLA’s directory. (See the “Problems Related to Class File Use” section in Appendix B, “Errors That Do Not Trigger an Error Message.”) This code must be in a file named C1.as that is in a subdirectory named C. C1.as in subdirectory C package C { public class C1 { public var var1:String=“from C.C1”; public function C1() { } } } Main.as package { import flash.display.MovieClip; // import C.C1 import C.C1; public class Main extends MovieClip { public function Main() { // create an instance (c1) of C1 var c1:C1 = new C1(); // because var1 is public property, you can access it from any // class that imports C.C1 trace(c1.var1); } } } Any variable accessed this way can also be redefined the same way, and that’s not always a good thing. For example, in Main, instead of just checking c1.var1, you could reassign it. c1.var1 = “from Class B”; There are two main reasons why you might not want to use a public variable and thereby allow this direct access to your C.C1 variable var1. First, you may want var1 to always be exactly “from C.C1” (in other words, you want it to be read-only). Second, you might choose to allow it to be changed, but you might always want it to indicate that it’s from C1.C (in other words, you want to error check). These are the two main reasons why you should know about getters and setters. 33
34 Chapter 3 n Writing Class Code Getters and Setters You might not have realized that you’ve been using a getter every time you check (get) a MovieClip’s x property and using a setter every time you assign (set) a MovieClip’s x property. All MovieClip properties that you have checked and assigned with dot notation are examples of how to use getters and setters. Adobe wrote the MovieClip class code that defined those getters and setters. Here’s how to create your own getter: C1.as package { public class C1 { private var _var1:String=“from C1”; public function C1() { } public function get var1():String{ trace(“getter working”); return _var1; } } } Notice that I changed the variable to _var1 and made it private. It can’t be accessed from anywhere outside of the class C1 directly (by referencing a C1 class member). But I did define a public function get var1() that returns the value of _var1. Now, if in Main I try the following, I’ll see the indicated trace() function output. Main.as package { import flash.display.MovieClip; public class Main extends MovieClip { public function Main() { var c1:C1 = new C1(); // “getter working” followed by “from C1” is traced trace(c1.var1); // If you uncomment the line below, a 1059 error will be // triggered. (See Figure 3.5.) // c1.var1 = “from B”; } } }
Getters and Setters Figure 3.5 Error triggered by trying to write to a read-only property. Source: Adobe Systems Incorporated. Here’s how to add a setter that does error checking and makes var1 read and write: C1.as package C{ public class C1 { private var _var1:String=“from C1”; public function C1() { } public function get var1():String{ return _var1; } public function set var1(s:String):void{ // allow anything to be appended to _var1 if(s.indexOf(“from C1”)==0){ _var1 = s; } else { trace(“Invalid assignment to _var1 attempted”); } } } } Now, if in Main I try the following, I’ll see the indicated trace() function output. 35
36 Chapter 3 n Writing Class Code Main.as package { import flash.display.MovieClip; public class Main extends MovieClip { public function Main() { var c1:C1 = new C1(); // “getter working” followed by “from C1” is traced trace(c1.var1); // “Invalid assignment to _var1 attempted” is traced and var1 is // not reassigned. c1.var1 = “from B”; // will reassign _var1 c1.var1 = “from C1 reassigned in Main”; } } } If you have two other classes C2 and C3 and you want to reference var1 from C1 in each of those, you would use similar code in each class. But, the two var1 properties aren’t the same, and they probably won’t have the same value. Each is a property of its class instance, and you’ll be creating one instance in C2 and a different instance in C3. Even if you call them both c1, they’re not in the same scope, and therefore they don’t conflict with each other and aren’t the same instances. Most of the time, that’s exactly what you want: Each C1 instance is distinct from every other C1 instance. But there are ways to create a property in one class, assign it a value, and enable all other classes in your project to access that same property. One way is to use a static property. But before I get to static properties, I want to expand on the previous Main and C1 example by showing three ways to communicate from Main to C1. The previous sample code showed you how to communicate from C1 to Main. Among the ways you can communicate from Main to C1, you can pass a variable from Main in the C1 constructor, you can use a public method in C1 to pass a variable, and you can use an event dispatcher in Main to communicate with an event listener in C1. Example 1: Passing a Variable from Main to C1 via the C1 Constructor Main.as package { import flash.display.MovieClip
Getters and Setters public class Main extends MovieClip { private var mainVariable:String = “from Main”; public function C2() { var c1:C1 = new C1(mainVariable); } } } C1.as package { private var mainVar:String; public class C1 { public function C1(_mainVar:String) { mainVar=_mainVar; } } } Example 2: Passing a Variable from Main to C1 Using a C1 Public Method Main.as package { public class Main { private var mainVariable:String = “from Main”; public function Main() { var c1:C1 = new C1(); c1.getVarF(mainVariable); } } } C1.as package { private var mainVar:String; public class C1 { public function C1() { } public function getVarF(_mainVar:String){ mainVar = _mainVar; } } } 37
38 Chapter 3 n Writing Class Code Example 3: Using an Event Dispatcher Main.as package { import flash.display.MovieClip; import flash.events.Event; public class Main extends MovieClip { public function Main() { var c1:C1 = new C1(); c1.dispatchEvent(new Event(“customE”)); } } } C1.as package { // If you fail to import the Event class, you will trigger a 1046 error. (See // Figure 3.6.) import flash.events.Event; import flash.events.EventDispatcher; // C1 must extend a class that has an addEventListener method or you’ll trigger // a 1061 error. (See Figure 3.7.) // If you do not know which classes have an addEventListener method, use // the Flash help files > ActionScript 3.0 Language reference > index > // addEventListener. The classes with this method are listed. public class C1 extends EventDispatcher{ public function C1() { this.addEventListener(“customE”, customF); } private function customF(e:Event):void { trace(e.type+“ has been dispatched from in Main”); } } }
static Properties Figure 3.6 Error triggered by trying to reference an object (e) that is a member of an unavailable (to C1) class (Event). Source: Adobe Systems Incorporated. Figure 3.7 Error triggered by trying to apply a method to C1 instances that is not part of the C1 class. Source: Adobe Systems Incorporated. static Properties properties are assigned to a class, not a class instance. When you use the new constructor, you create class instances. When you want to reference a static property, you reference the class and property using dot notation without using a constructor. static To show how this works, start by creating a document class Main. Click an empty part of the stage or pasteboard and type Main in the Document Class textfield in the Properties panel. Then click the pencil icon (Edit Class Definition) to the right of that textfield. Replace the Main class code created by Flash with the Main class below and save the file as Main.as. 39
40 Chapter 3 n Writing Class Code Main.as package { import flash.display.MovieClip; import flash.display.DisplayObject; public class Main extends MovieClip { public function Main() { // Glo and TestClass need to be created before testing the Main // class var glo:Glo = new Glo(MovieClip(this.root)); var tc:TestClass = new TestClass(); } } } Then create a code. Glo class (click File > New > ActionScript 3.0 Class) with the following Glo.as package { import flash.display.MovieClip; public class Glo { // bal is a static property of type object. public static var bal:Object={}; public function Glo(mc:MovieClip) { bal.root = MovieClip(mc.root); } } } And finally, create a third class (TestClass) to test whether you can reference bal.root in another class. TestClass1.as package { import flash.display.MovieClip; public class TestClass1 { private var rootVar:MovieClip; public function TestClass1() { rootVar = Glo.bal.root; trace(“test”,rootVar); } } }
static Properties This Glo.bal code to create pseudo-global properties isn’t my code. I saw it or something very similar on the Internet years ago, when Adobe first released Flash Pro CS3 and ActionScript 3.0. It’s probably Grant Skinner’s code, but I’m not certain. The key point isn’t that you should use pseudo-global properties or whose code that is. The key point is that the above shows one way to share the same property across any number of classes using a static property. You can create TestClass2, TestClass3, and so on, and they can all reference the same property. By the way, your document class must extend the Sprite or MovieClip class. If your document class extends the Sprite class instead of the MovieClip class, you’ll save an insignificant amount of memory. However, if your document class extends the Sprite class, you cannot use any timeline code (for debugging and testing), and even a single space or carriage return anywhere in the Actions panel will trigger an 1180 error (see Figure 3.8). Figure 3.8 Error triggered if the document class extends the Sprite class and there is anything in the Actions panel. Source: Adobe Systems Incorporated. If you’re testing the above three classes when your document class extends the Sprite class instead of the MovieClip class, you’ll need to replace all MovieClip references in all three classes with Sprite references. 41
42 Chapter 3 n Writing Class Code Internal, Private, Protected, Public, and Static Methods There’s no significant difference between applying internal, private, protected, and public attributes to properties and applying the same attributes to methods. You use the same syntax, and the same principles apply. Example 1: public Method C1.as package { public class C1 { public function C1() { } public function f1():void{ trace(“f1() in C1”); } } } C2.as package { public class C2 { public function C2() { var c1:C1 = new C1(); c1.f1(); } } } // create an instance (c1) of C1 Example 2: static public Method package { public class C1 { static public function f1():void{ trace(“f1 in C1”); } } }
Singleton Class C2.as package { public class C2 { public function C2() { C1.f1(); } } } Singleton Class This code is definitely from Grant Skinner and provides another way (in addition to using a class with static methods and properties) to share the variables and functions across any number of classes. SingletonDemo.as package { public class SingletonDemo { private static var instance:SingletonDemo; private static var allowInstantiation:Boolean; public static function getInstance():SingletonDemo { if (instance == null) { allowInstantiation=true; instance = new SingletonDemo(); allowInstantiation=false; } return instance; } public function SingletonDemo():void { if (! allowInstantiation) { throw new Error(“Error: Instantiation failed: Use SingletonDemo.getInstance() instead of new.”); } } } } To use a singleton class, you execute: var sd:SingletonDemo = SingletonDemo.getInstance(); In every class in which you execute SingletonDemo.getInstance(), the same class instance is returned. That is, if you execute that SingletonDemo.getInstance() in different classes, they will all return the same class instance. 43
44 Chapter 3 n Writing Class Code Because you can create only a single class instance using this construct, it’s called a singleton class. Here’s an example that shows how you could use this class to share data between Main and C1. Data is the singleton class. Main.as package { import flash.display.Sprite; import flash.events.Event; public class Main extends Sprite { private var test1:String = “test”; public function Main() { var c1:C1 = new C1(); // Because this is the first time Data.getInstance() is executed, // this creates the (only) Data instance var d:Data = Data.getInstance(); // Assign a variable’s value d.data1 = “HI”; // Notify c1 that data1 has had its value changed. c1.dispatchEvent(new Event(“data1ChangeE”)); } } } C1.as package { import flash.events.Event; import flash.events.EventDispatcher; public class C1 extends EventDispatcher{ private var dataInstance:Data; public function C1() { // Retrieve the Data instance created in Main. It does not matter // what name you call the Data instance because the name is local // to C1. What matters is the instance itself is the same. dataInstance = Data.getInstance(); this.addEventListener(“data1ChangeE”, data1ChangedF); } private function data1ChangedF(e:Event):void { // Retrieve the data1 property of the Data instance. trace(dataInstance.data1); } } }
dynamic Class Data.as package { public class Data { private static var instance:Data; private static var allowInstantiation:Boolean; private var _data1:String; public static function getInstance():Data { if (instance == null) { allowInstantiation=true; instance = new Data(); allowInstantiation=false; } return instance; } public function Data():void { if (! allowInstantiation) { throw new Error(“Error: Instantiation failed: Use Data.getInstance() instead of new.”); } } public function set data1(s:String):void { _data1=s; } public function get data1():String { return _data1; } } } dynamic Class Normally, when a class instance is created, you cannot assign properties to that instance unless they are declared in the class and have a suitable (or at least not private) attribute. If you try, you will trigger a 1119 error (see Figure 3.9). For example: Main.as package { import flash.display.Sprite; import flash.events.Event; public class Main extends Sprite { private var test1:String = “test”; public function Main() { 45
46 Chapter 3 n Writing Class Code var c1:C1 = new C1(); // An attempt to assign var1 to c1 will trigger a 1119 error. c1.var1 = “HI”; } } } C1.as package { public class C1{ public function C1() { } } } Figure 3.9 Error triggered if you try to assign an undeclared property. Source: Adobe Systems Incorporated. However, if you use the class modifier dynamic, you can assign properties undeclared in C1 outside of C1. For example, changing C1 to the following will allow var1 to be applied to c1 in Main. C1.as package { dynamic public class C1{ public function C1() { } } }
Chapter 4 What You Should Know While developing your game, you can easily learn much of what you need to know to encode games just by using logic and knowing what document to use as a reference. Those topics are covered in the next chapter. But some of what you should know is difficult to find unless you know it exists. I’ll briefly cover those in this chapter, listed in alphabetical order. You need to know they exist so that when the need arises, you remember these classes are available for your use. Then you can turn to the Flash API (discussed in the next chapter) to learn how to use them. Arrays When you need to save and retrieve a sequence of data, you might want to use a numerically indexed Array (or Array, for short). Much of the time, you will save and retrieve one variable at a time. For example: var mc:MovieClip = new MovieClip(); creates one MovieClip that is referenced by mc. But what if you create 100 MovieClips? // for loops are another one of those concepts you need to know and are // mentioned below in the Loops section. for(var i:int=0;i<100;i++){ var mc:MovieClip = new MovieClip(); } 47
48 Chapter 4 n What You Should Know How can you reference each of those 100 MovieClips? One way is to use an Array. For example: var mcA:Array = []; for(var i:int=0;i<100;i++){ var mc:MovieClip = new MovieClip(); mcA.push(mc); } now contains a reference for each of the 100 MovieClips. For example, mcA[0] references the first created MovieClip in that for loop. In fact, because of the code I used, it is the only way to reference that MovieClip and all of the other MovieClips (other than the last one created). mc still references that last MovieClip even after the for loop completes. mcA Better coding to create 100 MovieClips stored in mcA would be: var mcA:Array = []; for(var i:int=0;i<100;i++){ // push is method of the Array class that adds elements to an array. mcA.push(new MovieClip()); } because Flash then isn’t burdened with the creation and disposal of 99 mc references. There is a useful relationship between the String class and the Array class. Any String instance can be converted to an Array instance using the split() method of the String class, and any Array instance can be converted to a String using the join() method of the Array class. For example, I had a project where I wanted to create an Array containing each letter in the alphabet. This would be boring, time-consuming, and prone to typos: var alphabetA:Array = [“a”,”b”,”c”,...,”z“]; If I tried doing that, I would probably end up with two commas next to each other and/or mismatched quotes. Instead, I used the following, which is much more efficient and less likely to need debugging because of typos: var alphabetS:String = “abcdefghijklmnopqrstuvwxyz“; var alphabetA:Array = alphabetS.split(““); When you want to convert an Array instance into a string, use the join() // join is method of the Array class that concatenates the array elements // in a string separated by the join parameter. In this example, there is method:
Arrays // no parameter used. var s:String = alphabetA.join(““); This should create a string s that is identical to alphabetS. By default, assignment of an Array to another Array creates a “deep” copy. That is, changes to one Array affect the other Array. For example: var a:Array = [1,2]; var b:Array = a; a.push(3) b.push(4); trace(a); // traces 1,2,3,4 trace(b); // traces 1,2,3,4 To create a copy of an Array that you can manipulate independently from the original, use the slice() or concat() method. The concat() method is faster. var a:Array = [1,2]; var b:Array = a.concat(); // or var b:Array = a.slice(); a.push(3) b.push(4); trace(a); // traces 1,2,3 trace(b); // traces 1,2,4 It isn’t unusual to have a list of numbers or objects from which you want to select an element at random repeatedly, without selecting the same number or object more than once. This is analogous to shuffling a deck of cards. For those situations, adding the numbers or objects to an Array and then using the shuffle() function (an implementation of the Fisher-Yates shuffle method) as follows is ideal. function shuffle(a:Array) { var j:int; var i:int; var e:*; var len:int = a.length; for (i = len-1; i>=0; i--) { // Check Randomization below in the Math Class if you are // unfamiliar with Math.random() j=Math.floor((i+1)*Math.random()); e = a[i]; a[i] = a[j]; a[j] = e; } } 49
50 Chapter 4 n What You Should Know To use shuffle(), make no changes to the function. Simply pass the Array you wish to randomize to shuffle() and then iterate through the shuffled function. For example: var Array1:Array=[0,1,2,3,4,5,6,7,8,9]; shuffle(Array1); // Array1 is now randomized. If you iterate through the // elements of the now randomized Array1, you will generate 0 to 9 in // random order without repeats. trace(Array1); Associative Arrays In ActionScript, an associative Array is another term for an Object and is somewhat similar to an Array in that Arrays can be thought of as an ordered collection of references (from the first Array element to the last), while associative Arrays are an unordered collection of references. In other programming languages, associative Arrays are also called maps or hashes. No matter what they are called, associative Arrays associate strings and values (while regular Arrays associate non-negative integers with values). The strings are used to point to a value and are called keys. For example: var mcObj:Object = {}; for(var i:int=0;i<100;i++){ var mc:MovieClip = new MovieClip(); mc.name = “mc_“+i; mcObj[mc.name] = mc; // mcObj[mc] = mc; will not work the way you want. Check the // Dictionary class. } trace(mcObj[“mc_0“],mcObj[“mc_0“].name); // [object MovieClip] mc_0 creates and defines an associative value is a reference to the actual an associative Array, you can use: that uses MovieClip names as keys, and the MovieClip that has that name. To iterate through Array for(var s:String in mcObj){ // mcObj[s] is using Array notation. See the next section. trace(“The key is”,s,”and the value is”,mcObj[s]); } Array Notation You use Array notation to coerce Flash to recognize a string as an object. For example, if you have on-stage MovieClips mc1, mc2,…mc25, this is in the scope of the
BitmapData 25 MovieClips, and you want to add a can use: MouseEvent.CLICK listener to them all, you for (var i:int=1; i<=25; i++) { this[“mc“+i].addEventListener(MouseEvent.CLICK,clickF); } function clickF(e:MouseEvent):void { trace(e.currentTarget.name); } Similarly, if you wanted to create variables you could use: a,b,c,...,z and initialize them all to 0, var alphabetS:String = “abcdefghijklmnopqrstuvwxyz“; var alphabetA:Array = alphabetS.split(““); for(var index:int=0;index<alphabetA.length;index++){ // The outer brackets are an example of using Array notation to coerce // a string to an object, in this case, a variable. The inner brackets // display the standard Array access operator. this[alphabetA[index]] = 0; } trace(this.a,this.b,this.z); When I first wrote that code, I used i (my usual) as the iteration variable in this for loop and triggered a script timeout error because i was resetting the iteration variable i (to be 0) inside the for loop. BitmapData Any time you need to access, manipulate, or create pixel data, think of the BitmapData class. draw(), getPixel32(), setPixel32(), and hitTest() are the methods you should first study after checking the constructor. Among other things, with those methods you can create a color detector for your DisplayObjects and create a shape-based hit test. For example, here’s a simple color picker for a DisplayObject with a top-left registration point. In this example, the clicked pixel in mc is the traced color. var bmd:BitmapData = new BitmapData(mc.width,mc.height,true,0x00000000); var mat:Matrix = mc.transform.concatenatedMatrix; mat.tx=0; mat.ty=0; bmd.draw(mc,mat); stage.addEventListener(MouseEvent.CLICK,clickF); function clickF(e:MouseEvent):void{ 51
52 Chapter 4 n What You Should Know trace(bmd.getPixel32(e.stageX-mc.x,e.stageY-mc.y).toString(16)); } And here’s a more complex application for a shape-based hit-detection class. This class is an example of a singleton class. To use it, encode: // import the needed class import com.kglad.HitF; // Create a class instance using the following line of code. This ensures // only one class instance is created in your game. var hitF:HitF = HitF.getInstance(); // To test for a shape-based hit between two objects mc1 and mc2, use: if(hitF.hit(mc1,mc2)){ // do whatever } The class itself follows. It contains a lot of code that may not be needed in specific situations. Because this class allows for any number of object nestings and any registration points for those nestings and it minimizes the number of created bitmaps, it is fairly complex, and each bitmap is stage-sized. In many situations, stage-sized bitmaps will be undesirable (because of the memory requirements). The benefit is that the class works in all situations (known to me), whereas the better-known CollisionDetection class by Grant Skinner fails in situations where objects are nested and/or transformed. package com.kglad{ import flash.display.BitmapData; import flash.geom.*; import flash.display.DisplayObject; import flash.utils.getQualifiedClassName; import flash.display.Bitmap; public class HitF{ public static var instance:HitF; private var dataObj:Object; public static function getInstance():HitF { if (instance==null) { instance = new HitF( new SingletonEnforcer() ); } return instance; } public function HitF( pvt:SingletonEnforcer ) { dataObj = {}; }
BitmapData public function hit(dobj1:DisplayObject,dobj2:DisplayObject):Boolean{ if(!dobj1 || !dobj2){ return false; } if(!dobj1.stage || !dobj2.stage){ return false; } if(!dobj1.hitTestObject(dobj2)){ return false; } if(!dataObj[dobj1.name]){ dataObj[dobj1.name] = {}; } if(!dataObj[dobj2.name]){ dataObj[dobj2.name] = {}; } dataObj[dobj1.name].currentMat = dobj1.transform.concatenatedMatrix; dataObj[dobj2.name].currentMat = dobj2.transform.concatenatedMatrix; checkForChangeF(dobj1); checkForChangeF(dobj2); // if(dataObj[dobj1.name]["bmpd"].hitTest(new Point(0,0),255,dataObj[dobj2.name]["bmpd"],new Point(0,0),255)){ return true; } else { return false; } } private function checkForChangeF(dobj:DisplayObject):void{ var createBMPDBool:Boolean = false; if(getQualifiedClassName(dobj).indexOf("MovieClip")>-1){ if(dataObj[dobj.name].previousFrame!=dataObj[dobj.name].currentFrame){ dataObj[dobj.name].previousFrame = dataObj[dobj.name].currentFrame; createBMPDBool = true; } } if (!dataObj[dobj.name]["bmpd"] || dataObj[dobj.name].currentMat.tx != dataObj[dobj.name].previousMat.tx || dataObj[dobj.name].currentMat.ty != dataObj[dobj.name].previousMat.ty || dataObj[dobj.name].currentMat.a != dataObj[dobj.name].previousMat.a || 53
54 Chapter 4 n What You Should Know dataObj[dobj.name].currentMat.b != dataObj[dobj.name].previousMat.b dataObj[dobj.name].currentMat.c != dataObj[dobj.name].previousMat.c dataObj[dobj.name].currentMat.d != dataObj[dobj.name].previousMat.d) { || || dataObj[dobj.name].previousMat = dataObj[dobj.name].currentMat; createBMPDBool = true; } if(createBMPDBool){ createBmpdF(dobj); } } private function createBmpdF(dobj:DisplayObject):void{ if(dataObj[dobj.name]["bmpd"]){ dataObj[dobj.name]["bmpd"].dispose(); } dataObj[dobj.name]["bmpd"] = new BitmapData(dobj.stage.stageWidth,dobj.stage.stageHeight,true,0x22000000); dataObj[dobj.name]["bmpd"].draw(dobj,dataObj[dobj.name].currentMat); } public function clearF(dobj:DisplayObject):void{ if(dataObj[dobj.name]){ delete dataObj[dobj.name]; } } } } internal class SingletonEnforcer{} Conditional Compiling Conditional compiling allows you to compile different code blocks to your ActionScript depending on the value(s) of certain configuration constants. For example, you can have blocks of code that are compiled only when you’re publishing for the web and different blocks of code that are compiled only when you’re publishing for an Android device in the same class file, along with code that is published for both situations. You use CONFIG::xx constants to indicate a code block, and you assign the constants a Boolean in the Config Constants panel: File > Publish Settings > ActionScript 3.0
Dictionary Settings > Config Constants (see Figure 11.16 in Chapter 11, “Social Gaming: Social Networks”). For an example using web and Android publishing, in the same (or different) class files you could have: CONFIG::ANDROID{ // some block of code published only when CONFIG::ANDROID is true } CONFIG::WEB{ // some block of code published only when CONFIG::WEB is true } You add CONFIG::ANDROID and CONFIG::WEB in the Config Constants panel. By assigning true or false to each configuration constant and then publishing, you control whether one, both, or neither code block is included in the compiled code. We’ll discuss this further in Chapter 11. Dictionary In ActionScript, if you think of an associative Array as a generalized indexed Array, then a Dictionary is a generalized associative Array. Where a standard Array accepts non-negative integers for keys and an associative Array accepts strings for keys, a Dictionary accepts objects for keys. Notice that if you want to store objects as keys, using an associative you the results you expect. Array won’t give var mc1:MovieClip = new MovieClip(); var mc2:MovieClip = new MovieClip(); var obj:Object = {}; obj[mc1]=“a“; obj[mc2]=“b“; // This traces true trace(obj[mc1]==obj[mc2]); var d:Dictionary = new Dictionary(); d[mc1] = “a“; d[mc2] = “b“; // This traces false trace(d[mc1]==d[mc2]); I’ll make use of a static Dictionary in Chapter 7, “Optimizing Game Performance,” where I use it to store object references for memory tracking. 55
56 Chapter 4 n What You Should Know DispatchEvent A DispatchEvent is a very useful way to communicate between two related classes. When you create a class C instance in class P and something occurs in class C that you want communicated to P, you can dispatch a custom event in C and listen for that event in P. For example: In P: var c:C = new C(); c.addEventListener(“customE”,customF); private function customF(e:Event):void{ // something occurred to c, in C } In C: private function f():void{ // something happened in C dispatchEvent(new Event(“customE“)); } If C is a class that extends any of the EventDispatcher subclasses, you need not use an statement. Otherwise, at the top of your class file C, use: import import flash.events.EventDispatcher; ExternalInterface The ExternalInterface class allows communication between ActionScript and the SWF container. Typically, the container is the embedding HTML page, and the communication is between ActionScript and JavaScript. Communication from ActionScript to JavaScript is especially easy using the static call() method of the ExternalInterface class. You just use: ExternalInterface.call(“some_JavaScript_function”,some_ActionScript_variable); to pass some_ActionScript_variable to some_JavaScript_function in the embedding HTML. You can do quite a bit more with the ExternalInterface call() method, including calling JavaScript functions created with ActionScript. For example: var jsXML:XML = <![CDATA[ function(s){ function nestJS_F(sVar){ return sVar+” augmented“; };
Listeners versus Weak Listeners return nestedJS_F(s); } ]]> var myResult = ExternalInterface.call(jsXML , “String from Flash“); trace(myResult); You can also call ActionScript functions from JavaScript using the static ExternalInterface addCallback() method. This is more complicated to use because not all HTML code that properly embeds a SWF will allow callbacks. You’ll find more details about this in Chapter 11, “Social Gaming: Social Networks.” Garbage Collection Garbage collection is a process used by Flash to clear memory of unneeded storage. Flash has a very rigorous method for determining what is unneeded. Chapter 7, “Optimizing Game Performance,” details this method. Briefly, if objects are not in the display list and have no references and no listeners, Flash marks them for removal. In the text that follows, I use the abbreviation gc with and without various suffixes, such as gc’d and gc’ing, to refer to garbage-collection activities. getTimer() This returns the number of milliseconds the Flash Player has been running. This is one of the more useful utilities in Flash, especially for testing code. For example, to test how quickly a block of code executes, you can use: var startTime:int = getTimer(); /* code block */ trace(getTimer()-startTime); Listeners versus Weak Listeners A weak listener is created using an addEventListener() method that doesn’t prevent gc’ing the object to which it is applied. The default (strong) listener does prevent gc’ing the listener object. The only difference between the two listeners is that the weak listener has a fifth parameter in addEventListener() assigned to true. The first parameter is the event type, and the second parameter is the listener function. The third parameter is a Boolean that determines whether to use the capture phase of this event. There may be occasions when you find this useful, but I never have. 57
58 Chapter 4 n What You Should Know Its only purpose would be if you want a parent listener to execute before a child listener or because you want to toggle the child listener function by toggling the event’s propagation. In any case, to use the fifth parameter, you have to use the third and fourth parameters. The fourth parameter is an int that indicates the priority of the listener. Again, there may be occasions when you find this useful, but I never have. Its only purpose would be if you have more than one listener applied to the same object and you want to control the order in which their listener functions execute. Normally, the listener that is added first calls its listener function first. If you need to control the call order dynamically, use the fourth parameter. // The usual listener mc.addEventListener(MouseEvent.CLICK,f); // The same listener using the default third and fourth parameters and a // boolean indicating it is a weak listener mc.addEventListener(MouseEvent.CLICK,f,false,0,true); But you cannot depend on either behavior (allowing gc’ing when using weak listeners and preventing gc’ing when using a strong listener) because of differences in how listeners are handled in different Flash Player versions. We’ll discuss this further in Chapter 7. Loops There are two significantly different loop types in ActionScript: one type that executes from start to end before any other code executes and before anything updates on-stage, and one type that does neither of those things. do Loops, for Loops, and while Loops Loop types that execute from start to end before anything updates on-stage include do loops, for loops, and while loops. They cannot be used to animate objects because no matter what code you use to execute the loop and no matter what you do to try to slow it down, it will still execute from start to finish before anything changes onstage. They are appropriately used for rapid execution of code. However, there are situations when you might want to break these loops into more than one chunk so the stage can update between each chunk. Chunks For example, if you have a for loop that takes 10 seconds to execute, your game will appear to freeze for 10 seconds after this loop starts. Nothing will update on-stage,
Loops and nothing will respond to user input. Either you should warn your user before starting that loop or you should break that loop into several smaller chunks that allow visual updates to users so they don’t think your game is broken. For example, this for loop that adds odd numbers (and shows the first bers sum to m*m) freezes my Flash Player for about 9 seconds. var var var var var var for m odd num- for loop) into i:Number; n:Number=3000000000; s:Number=0; startI:Number=1; endI:Number=n startTime:int=getTimer(); (i=startI; i<endI; i+=2) { s+=i; } // 9 seconds trace((getTimer()-startTime)/1000,s,n*n/4,s-n*n/4); The following technique shows how to break this (and any other chunks that allow the Flash Player to update every second. var i:Number; var n:Number=3000000000; var s:Number=0; var startTime:int=getTimer(); // This is the number of chunks into which the previous for loop will // be broken. If the previous for loop took about 9 seconds, using 10 // chunks means there will be updates about every 0.9 seconds. var chunks:int=10; var startI:Number=1; var endI:Number=n/chunks; var t:Timer=new Timer(100,1); t.addEventListener(TimerEvent.TIMER,f); f(); function f(e:Event=null):void { for (i=startI; i<endI; i+=2) { s+=i; } trace(“stage update”,startI,endI,s); if (endI<n) { t.reset(); t.start(); } else { trace((getTimer()-startTime)/1000,s,n*n/4,s-n*n/4); 59
60 Chapter 4 n What You Should Know } startI+=n/chunks; endI+=n/chunks; } break There may be occasions when you’ll want to terminate one of these loops when a condition is met. For these situations, you can use the break statement. for(var i:int=0;i<1000;i++){ if(i>10){ break; } trace(i); } If you want to terminate a nested for loop, name your loop and use that name in the break statement. For example, compare: for(var i:int=0;i<5;i++){ for(var j:int=0;j<5;j++){ if(i==2 && j==3){ break; } trace(i,j); } } to: outsideLoop: for(var i:int=0;i<5;i++){ for(var j:int=0;j<5;j++){ if(i==2 && j==3){ break outsideLoop; } trace(i,j); } } I will discuss do loops, for loops, and while loops again in Chapter 7. enterFrame, setInterval(), and Timer Loops loops, setInterval() loops, and Timer loops make up the second loop type. These are appropriate for animating on-stage objects using ActionScript. enterFrame
Math Class The Flash Player attempts to maintain a frequency of 1/stage.frameRate for the enterFrame loop. How successful the Flash Player is in maintaining that frequency depends on a number of factors, which I’ll explain in detail in Chapter 7. An important point to remember about these loops is that, at best, the average loop frequency will be accurate to within a few percent of the frequency the loop is intended to execute. However, the timing between consecutive loops can and will vary widely. At best, consecutive loops will execute at between 50 percent and 150 percent of the intended frequency. I recommend avoiding setInterval() (because it’s easy to misuse), and it’s no more accurate than the other loops. If you’re tempted to use it, at least execute clearInterval() before every setInterval(). var whateverI:int; clearInterval(whateverI); whateverI = setInterval(f,100); function f():void{ } Math Class This section covers facts and topics in geometry, linear interpolation, the modulo operator, randomizing and trigonometry. These topics occur repeatedly in many types of games. Becoming familiar with these topics and knowing the listed facts will help you create games. Geometry Let’s start with four basic assumptions from plane geometry. n A straight angle is 180 degrees (see Figure 4.1). Figure 4.1 a = 180. Source: © 2013 Keith Gladstien, All Rights Reserved. n The sum of a triangle’s angles is 180 degrees (see Figure 4.2). Figure 4.2 a + b + c = 180. Source: © 2013 Keith Gladstien, All Rights Reserved. 61
62 Chapter 4 n What You Should Know n Vertical angles are equal (see Figure 4.3). Vertical angles are two nonadjacent angles formed by two intersecting lines. Figure 4.3 a = c. Source: © 2013 Keith Gladstien, All Rights Reserved. n Adjacent angles formed by two intersecting lines sum to 180 degrees (see Figure 4.4). Adjacent angles are neighboring angles (i.e., they share a side and vertex formed by two intersecting lines. Figure 4.4 b + c = 180. Source: © 2013 Keith Gladstien, All Rights Reserved. Angles of Reflection These four plane geometry facts are basic. But, armed with those facts and using logic, you can solve several practical problems encountered in game programming. For example, in a game in which objects bounce off walls, you can determine the new angle of motion. More precisely, given an object at A moving along a straight line at angle b toward a vertical wall at B that reflects (or bounces) off the wall toward C (like a billiard ball), what angle should the object move along after reflection? (See Figure 4.5.)
Math Class Figure 4.5 First, where is angle b and where is the angle of reflection? (See Figure 4.6.) Source: © 2013 Keith Gladstien, All Rights Reserved. Figure 4.6 Angle b and angle of reflection r. Source: © 2013 Keith Gladstien, All Rights Reserved. What is angle r? Because: b + 90 + c = 180 c = 90 − b and r = c + 90 r = 90 − b + 90 = 180 − b See Figure 4.7. 63
64 Chapter 4 n What You Should Know Figure 4.7 r = c + 90 and c = 90 − b; therefore r = 180 − b. Source: © 2013 Keith Gladstien, All Rights Reserved. This argument is valid if b is between 0 and 90 degrees. Similar arguments show this statement is true for all angles of b. Using a similar (or simpler) argument, you should be able to show that an object moving along a straight line at angle b that reflects off of a horizontal wall moves along a line at angle 360 − b after reflection. Pythagorean Theorem In a right triangle, which is a triangle with a 90-degree angle, the sum of the sides’ lengths squared is equal to the hypotenuse’s length squared (see Figure 4.8). Figure 4.8 C*C=A*A+B*B Source: © 2013 Keith Gladstien, All Rights Reserved. Most commonly, this is used in Flash games when calculating the distance between two on-stage objects. For example, determining the distance between the registration points of mc1 and mc2 is a direct application of the Pythagorean theorem. See Figure 4.9. var distance:Number = Math.sqrt((mc1.x-mc2.x)*(mc1.x-mc2.x)+(mc1.y-mc2.y) * (mc1.y-mc2.y));
Math Class Figure 4.9 If the leftmost green dot is mc1" and the other is mc2", then C = distance between mc1" and mc2", A = mc2.x" − mc1.x", and B = mc2.y − mc1.y. Source: © 2013 Keith Gladstien, All Rights Reserved. Linear Interpolation It’s surprising how many situations lend themselves to linear interpolation. Briefly stated, any time you have two pairs of related numbers, you can define a linear relationship between the two pairs and extend the relationship to any number of pairs. For example, if you have a masked vertical DisplayObject (for example, mc) that you want to scroll up and down using a horizontal scrollbar (for example, sb), and all your objects have top-left registration, you can use: // Align mc’s top with mask_mc’s top mc.y=mask_mc.y; mc.mask=mask_mc; // Create the rectangle to define the region where the scrollbar’s // thumb will drag var rect:Rectangle = new Rectangle(0,0,sb.width-sb.thumb.width,0); // Assign listeners sb.thumb.addEventListener(MouseEvent.MOUSE_DOWN,downF); sb.thumb.addEventListener(MouseEvent.MOUSE_UP,upF); // paramF() is the function that defines the parameters used for scrolling. // We want sb.thumb’s x position to control mc’s y parameter. We already // know two points in this relationship. // The first is (0,mask_mc.y) when the thumb is to the far left (0), mc’s // top should align with mask_mc’s top (mc.y=mask_mc.y) and the second is // (sb.width-sb.thumb.width,mask_mc.y-mc.height+mask_mc.height) when // the thumb’s right side is to the far right of the scrollbar (sb.width// sb.thumb.width)), the mc’s bottom should align with mask_mc’s bottom. // We can now use linear interpolation to find the parameters to be used // to associate all the other x-values that sb.thumb can take with the // corresponding y-values of mc. paramF(sb.thumb,0,mask_mc.y,sb.width-sb.thumb.width,mask_mc.y-mc.height+mask_mc. height); 65
66 Chapter 4 n What You Should Know function downF(e:MouseEvent):void{ sb.thumb.startDrag(false,rect); sb.thumb.addEventListener(Event.ENTER_FRAME,scrollF); } function upF(e:MouseEvent):void{ sb.thumb.stopDrag(); sb.thumb.removeEventListener(Event.ENTER_FRAME,scrollF); } function scrollF(e:Event):void{ // Here is where the parameters found in paramF() are used to // assign mc’s y position mc.y = sb.thumb.m*sb.thumb.x+sb.thumb.b; } function paramF(mc:MovieClip,x1:Number,y1:Number,x2:Number,y2:Number):void{ mc.m = (y1-y2)/(x1-x2); mc.b = y1 - mc.m*x1; } You can use exactly the same code (with two lines of code changed) in many different situations. The two lines are: paramF(sb.thumb,0,mask_mc.y,sb.width-sb.thumb.width,mask_mc.ymc.height+mask_mc.height); and mc.y = sb.thumb.m*sb.thumb.x+sb.thumb.b; You aren’t limited to x and y properties. With few changes, you can use the same code any time you want a change in any property in one object to effect a change in some property in another object. In fact, you aren’t limited to associating properties. For example, you can control the timeline play of a MovieClip using a scrollbar (in other words, scroll a MovieClip timeline), and you can use a MovieClip’s timeline play to control an object property. Specifically, if you want to scroll a MovieClip (for example, cal scrollbar (for example, sb), use: mc) timeline using a verti- paramF(sb.thumb,sb.thumb.height,1,sb.height,mc.totalFrames); and in an Event.ENTER_FRAME listener function, use: mc.gotoAndStop(int(sb.m*sb.y+sb.b)); If you want the mc’s timeline to control vertical sb’s thumb, use: paramF(mc,1,sb.thumb.height,mc.totalFrames,sb.height);
Math Class and in an Event.ENTER_FRAME listener function, use: sb.thumb.y = mc.m*mc.currentFrame+mc.b; Modulo Operator The modulo operator (%) is very handy in several situations. You can use it in an if statement to filter certain values of the iteration variable. For example, if n is even and you want to add the first n/2 odd numbers and the first n/2 even numbers, you can use one for loop and the modulo operator. var i:Number; var n:Number=500000; var sOdd:Number=0; var sEven:Number=0; for(i=1;i<=n;i++){ // This checks for odd numbers if(i%2==1){ sOdd+=i; } else { sEven+=i; } } // confirm the odd numbers sum to n*n/4 and the even numbers sum to n*(n+2)/4 trace(sOdd,”=”,n*n/4,”||”,sEven,”=”,n*(n+2)/4); The modulo operator is also useful in tiling situations. For example, if you want to tile the stage, the following code shows how you can use the modulo operator (see Figure 4.10). var var var var var for } len:int=12; gap:int = 2; rows:int=Math.floor(stage.stageHeight/(len+gap)); cols:int=Math.floor(stage.stageWidth/(len+gap)); sp:Sprite; (var i:int=0; i<rows*cols; i++) { sp = new Sprite(); addChild(sp) with (sp.graphics) { beginFill(0xffffff*Math.random()); drawRect(0,0,len,len); endFill(); } sp.x = (i%cols)*(gap+sp.width); sp.y = Math.floor(i/cols)*(gap+sp.height) 67
68 Chapter 4 n What You Should Know Figure 4.10 Tiling any rectangular region with any tile shape is easy using the modulo operator. Source: Adobe Systems Incorporated. Randomizing It’s a little late to be explaining randomization, because I’ve already used it at least twice before this subsection. But, better late than never. Randomizing is such a key part of game creation that I doubt I’ve ever made a game that doesn’t use Math.random(). Math.random() returns a pseudo (or almost) random number greater than or equal to zero and less than one. Using Math.random(), you can return a random number between any two numbers. function randomF(n1:Number,n2:Number):Number{ if(n1<n2){ return n1+(n2-n1)*Math.random(); } else { return n2+(n1-n2)*Math.random(); } } For example, if you want to generate a random number between 2.2 and 2.3, you would use: var r1:Number = randomF(2.2,2.3);
Math Class If you wanted to return a random integer between any two integers, you could use: function randomF(n1:Number,n2:Number):int { // Error check to ensure two integers were passed. if(n1!=int(n1) || n2!=int(n2)){ return int.MIN_VALUE; } if (n1<n2) { return int(n1+(1+n2-n1)*Math.random()); } else { return int(n2+(1+n1-n2)*Math.random()); } } In the modulo operator example, I used Math.random() to generate a random color: var randomColor:uint = 0xffffff*Math.random(); To position an object (for example, mc) with an upper-left registration point at a random on-stage location, you can use: mc.x = (stage.stageWidth-mc.width)*Math.random(); mc.y = (stage.stageHeight-mc.height)*Math.random(); To go to a random frame in a MovieClip (for example, mc) timeline, you can use: mc.gotoAndStop(Math.ceil(Math.random()*mc.totalFrames)); Because Math.random() generates a number greater than or equal to zero and less than one, rounding up is appropriate. Math.ceil() should be used because it rounds up. Trigonometry You only need to know three things about trigonometry for Flash games: the sine of an angle, the cosine of an angle, and arctangent of a number. The sine and cosine are functions of an angle, and the arctangent is a function of a number. The sine and cosine relate angles with ratios of right triangle sides, while the arctangent relates the ratio of right triangle sides to angles. First, if you’re already familiar with trigonometry, you need to remember that zero degrees is east, not north, and positive angles in Flash are measured in the clockwise direction, just the opposite of what you learned studying mathematics. This actually makes sense because the positive y-axis in Flash is down, the opposite of what you learned in math class. 69
70 Chapter 4 n What You Should Know The following are definitions. n In a right triangle, the sine of an angle is the ratio of the side opposite the angle and the hypotenuse. n In a right triangle, the cosine of an angle is the ratio of the side adjacent to the angle and the hypotenuse. n In a right triangle, the arctangent of the ratio of opposite and adjacent sides to an angle b is b. (See Figure 4.11.) Figure 4.11 Sine of b = B/C, cosine of b = A/C, sine of a = A/C, cosine of a = B/C, arctangent of B/A = b. Source: © 2013 Keith Gladstien, All Rights Reserved. Notice that the absolute size of the triangle doesn’t matter. The lengths of the sides of the triangle in Figure 4.11 could be several angstroms or several light years, and they could shrink or expand. As long as the angles stay the same, the ratios of the sides will be the same. In ActionScript, the sine function is Math.sin(), and the cosine function is Math.cos(). The arctangent function is Math.atan(), which accepts the ratio of side length. Flash also has a convenient Math.atan2() function that accepts the lengths of the two (opposite and adjacent) sides instead of the ratio of those two sides like Math.atan(). and Math.cos() accept angles in radians, and Math.atan() and Math.atan2() return angles in radians. If you prefer to work in degrees, you can convert from radians to degrees by multiplying radians by 180/Math.PI and you can convert from degrees to radians by multiplying by Math.PI/180. Math.sin() So, how would you use these functions? Check Figure 4.11 and imagine you have an object at the vertex of angle b whose movement you want to animate to the vertex of angle a. To do this, you need to update your object’s x and y properties repeatedly using a loop (such as enterFrame) and using Math.sin() and Math.cos().
Math Class // The speed of movement var speed:int=3; // The x,y of vertex b var vertex_bX:int = 50; var vertex_bY:int = 50; // The x,y of vertex c var vertex_cX:int = 450; var vertex_cY:int = 250; // Calculate the angle between the two vertices b and c var b = Math.atan2(vertex_cY-vertex_bY,vertex_cX-vertex_bX); // Create an object var sp:Sprite = new Sprite(); with (sp.graphics){ beginFill(0x990000); drawCircle(0,0,10); endFill(); } addChild(sp); // Start sp at vertex b sp.x = vertex_bX; sp.y = vertex_bY; // Create a loop to animate (move) sp from vertex b to vertex c. this.addEventListener(Event.ENTER_FRAME,enterFrameF); function enterFrameF(e:Event):void{ // Update sp’s x and y properties sp.x += int(speed*Math.cos(b)); sp.y += int(speed*Math.sin(b)); // Stop sp’s movement when it reaches vertex c if(sp.x >= vertex_cX){ this.removeEventListener(Event.ENTER_FRAME,enterFrameF); } } All linear movement reduces to using the same ideas and code. 1. You generally have two points. 2. You calculate the angle between the two points using Math.atan2(). 3. You start a loop like Math.atan() or Event.ENTER_FRAME. 4. You update your object’s from 2. x property using Math.cos() of the calculated angle 71
72 Chapter 4 n What You Should Know 5. You update your object’s from 2. y property using 6. Check for an end loop and/or hitTest() Math.sin() of the calculated angle and/or boundary condition. 7. Take appropriate action given a positive test in 6. Here’s an example using an object bouncing off the stage boundaries. // Create an object var mc:MovieClip = new MovieClip(); with (mc.graphics){ beginFill(0x990000); drawCircle(0,0,10); endFill(); } addChild(mc); mc.cacheAsBitmap // The speed of movement var speed:int=8; initializeF(mc); function initializeF(mc:MovieClip):void{ // Assign initial position mc.x = mc.width/2+Math.random()*(stage.stageWidth-mc.width/2); mc.y = mc.height/2+Math.random()*(stage.stageHeight-mc.height/2); // Assign initial angle var angle:Number = 2*Math.PI*Math.random(); // Instead of repeatedly calculating the cosine and sine of the same // angle, do it once, and store the values in variables. This is much // faster than repeatedly calculating the same value. Even values like // the stage dimensions and object dimensions can be more efficiently // stored in variables. This will be discussed further in Chapter 7. mc.xVector = 1+int(speed*Math.cos(angle)); mc.yVector = 1+int(speed*Math.sin(angle)); } this.addEventListener(Event.ENTER_FRAME,enterFrameF); function enterFrameF(e:Event):void{ // Update mc’s x and y properties mc.x += mc.xVector; mc.y += mc.yVector; // Bounce off stage boundaries. if(mc.x >= stage.stageWidth-mc.width/2 || mc.x <= mc.width/2){ // This follows because cos(180-b) = -cos(b) and // sin(180-b) = sin(b), the proof of which is beyond the //scope of this book, but Figure 4.12 may suggest the
Math Class // validity of this assertion. mc.xVector *= -1; } if(mc.y >= stage.stageHeight-mc.height/2 || mc.y <= mc.height/2){ // This follows because cos(360-b) = cos(b) and // sin(360-b) = -sin(b), and again the proof of this is //beyond the scope of this book, but Figure 4.13 may //suggest the validity of this assertion. mc.yVector *= -1; } } Figure 4.12 Figure suggesting cos(180−b) = cos(b’) = −A/C = −cos(b) and sin(180−b) = sin(b’) = B/C = sin(b). Source: © 2013 Keith Gladstien, All Rights Reserved. Figure 4.13 Because 360−b = −b, figure suggesting cos(−b) = A/C = cos(b) and sin(−b) = −B/C = −sin(b). Source: © 2013 Keith Gladstien, All Rights Reserved. By the way, the Figures 4.12 and 4.13 illustrate how to convert from polar to rectangular coordinates. That is, given b and C, find A and B: A = C*Math.cos(b); B = C*Math.sin(b); 73
74 Chapter 4 n What You Should Know They also illustrate how to convert from rectangular to polar coordinates. That is, given A and B find C and b: C = Math.sqrt(A*A+B*B); b = Math.atan2(B,A); mouseOut versus rollOut The MouseEvent.MOUSE_OUT (= mouseOut) is dispatched when you mouse off of the object to which the listener is applied, even if you’re still over a child of the object. The MouseEvent.ROLL_OUT (= rollOut) is dispatched only when the mouse moves off the object and all its children. To see the difference, create a MovieClip that contains two shapes that overlap. Convert one of the shapes to a child MovieClip. Add the parent MovieClip to the stage and assign an instance name—for example, mc. Then use the following code to see how the two mouse events differ. mc.addEventListener(MouseEvent.MOUSE_OVER,f); mc.addEventListener(MouseEvent.MOUSE_OUT,f); mc.addEventListener(MouseEvent.ROLL_OVER,f); mc.addEventListener(MouseEvent.ROLL_OUT,f); function f(e:MouseEvent):void{ trace(e); } Pausing/Restarting Your Game You can use the frameRate property of the stage to pause and restart your game: var originalFR:int = stage.frameRate; pausePlayToggle.addEventListener(MouseEvent.CLICK,pausePlayF); function pausePlayF(e:MouseEvent):void{ // After setting to zero, the frameRate is actually reported as .01 if(stage.frameRate>.01){ stage.frameRate = 0 } else { stage.frameRate = 24; } } Registration Points All display objects have a registration point, which is used by Flash to position, rotate, and transform the object (when using ActionScript). When creating objects
Registration Points in the Flash IDE (integrated development environment), the registration point is denoted by the plus sign (+) when editing the symbol. For objects created with ActionScript, 0,0 is the object’s registration point. When using the drawing methods of an object’s graphics property, x and y parameters are required, and these are offsets from the object’s registration point. All display object positions, rotations, and transforms, when changed using ActionScript, are relative to the object’s registration point. In the IDE, transforms are relative to the transform point, which, by default, is the object’s center (but can be changed using the Transform tool). Here’s an example of how an object’s registration point and the drawing methods impact what is seen on-stage. var sp:Sprite = new Sprite(); var xVar:int = 40; var yVar:int = 20; with(sp.graphics){ beginFill(0xaa0000) drawCircle(xVar,yVar,15); endFill() } addChild(sp); this.addEventListener(Event.ENTER_FRAME,f); function f(e:Event):void{ sp.rotation+=3; } Change the values of xVar and yVar until you understand how sp’s registration point of 0,0 and drawCircle()’s first two parameters control what you see on-stage. With ActionScript 3.0, you can change the registration point of any display object using the following changeRegPt() function. That function accepts three parameters: the object reference, the new x registration point, and the new y registration point. To most easily see the result of changing an object’s registration point, just like in the previous example, use a loop that rotates your object. // Change mc’s registration point to its top left. // changeRegPt(mc,0,0); // Change mc’s registration point to its center // changeRegPt(mc,mc.width/2,mc.height/2); // Change mc’s registration point to its bottom right changeRegPt(mc,mc.width,mc.height); function changeRegPt(dobj:DisplayObjectContainer,x:Number,y:Number){ var r:Rectangle = dobj.getBounds(dobj); 75
76 Chapter 4 n What You Should Know for(var i:int=0;i<dobj.numChildren;i++){ dobj.getChildAt(i).x -= r.x+x; dobj.getChildAt(i).y -= r.y+y; } dobj.x += r.x+x; dobj.y += r.y+y; } // The code below is not needed to change an object’s registration point. // It is used to demonstrate the registration point has been changed. this.addEventListener(Event.ENTER_FRAME,rotateF); function rotateF(e:Event):void{ mc.rotation+=3; } SharedObject The Flash SharedObject acts like a browser cookie. You can store variables and values in a SharedObject, which is then stored on the user’s computer. Using this, you can store, for example, a game state so that when a user restarts your game, he or she can restart from the last saved game state. The SharedObject uses the static getLocal() method to create and retrieve an object saved on the user’s computer. You create a reference using: var so:SharedObject = SharedObject.getLocal(“your_game_so“); if(so.data.level){ trace(“this user’s last completed level was “+so.data.level); // start game from so.data.level } else { // start game from level 1 } // After each level is completed, update so.data.level. For example, // when level 1 is completed so.data.level = 2; // save your data update to the user’s hard drive by using the flush() method so.flush(); Tweening with ActionScript The Flash Tween class leaves a lot to be desired for several reasons. The main drawback is that you need to instantiate instances of the Tween class every time you want to tween something, and you need to instantiate different Tween instances for each property you want to tween.
Tweening with ActionScript Not only does that add housekeeping work for the Flash Player, but it also exposes a difficult-to-debug issue that occurs when you declare and instantiate a Tween instance inside a function. The Tween instance is local to the function and may be gc’d before the Tween completes terminating your object’s Tween. I recommend you avoid it and use one of the third-party tween classes. There are several good ones, but my preference is TweenLite. You can download GreenSock’s TweenLite at www.greensock.com/tweenlite. There are other GreenSock tweening classes that have more (and one with fewer) capabilities, but I’ve never needed anything more (or less) than TweenLite. There is documentation in the TweenLite.as file and easier to read documentation at www. greensock.com/as/docs/tween. To get started after downloading and extracting everything in the zip file, you can drag the com/greensock directory anywhere and use it in any project. Just import com.greensock.TweenLite and any easing class (like Back, Bounce, etc.) needed, and use the static TweenLite.to() and/or TweenLite.from(). Notice that you don’t create instances of GreenSock’s TweenLite. You use one of two static methods. That avoids the main drawback of the Flash Tween class. For example, to tween an object to 333, use: mc’s x property for 2 seconds from its current value import com.greensock.TweenLite; TweenLite.to(mc,2,{x:333}); // To tween for 1 second from 333 to mc’s current y, use TweenLite.from(mc,1,{y:333}); // To delay a tween for 4 seconds, use the delay parameter TweenLite.to(mc,1,{alpha:.2,delay:4 }); // To call a function completeF() when the tween completes, use TweenLite.to(mc,1,{rotation:12,onComplete:completeF}); You can also use other parameters, such as delay, useFrames, easeParams, immediateRender, onInit, onInitParams, onStart, onStartParams, onUpdate, onUpdateParams, onComplete, onCompleteParams, onReverseComplete, onReverseCompleteParams, overwrite, and paused. All are explained in the help files. I have rarely used most of those. I use the delay, onComplete, and onCompleteParams parameters frequently. Occasionally, I use onUpdate, onUpdateParams, and overwrite. But I’ve rarely, if ever, used the rest. So, there isn’t much to learn to master the use of TweenLite in almost all situations. 77
78 Chapter 4 n What You Should Know Variable Names You can name most objects whatever you choose, but you do have to follow some rules. First, ActionScript is case sensitive. So, myVar isn’t the same as myvar or MyVar. Second, use variable names that make sense and help your code be understandable. And, just because Flash allows you to use names such as: var Sprite:MovieClip = new MovieClip(); var int:Array = []; var Class:Sound = new Sound(); doesn’t mean you should. Third, you cannot use any non-alphanumeric characters other than the underscore (_) and dollar sign ($) in variable names. For example, you cannot use the dash: var mc1-mc2:MovieClip = new MovieClip(); var mc1.mc2:MovieClip = new MovieClip(); Fourth, you cannot start variable names with anything other than a letter, a dollar sign, or an underscore. That is, you cannot start a variable name with a number. So, the following will trigger an error: var 1var:MovieClip = new MovieClip(); Finally, there are some (key) words Flash absolutely will not let you use in any manner other than the one intended. For example, you cannot redefine the following key words: as, break, class, for, if, in, internal, is, package, private, protected, public switch, var, while The same holds true for all class properties, such as x and y. You will see an error 1084: Syntax Error: Expecting Identifier before xxx, if xxx is a key word. If xxx is a class property, you will see an error 1152: A Conflict Exists with Inherited Definition some.package.zzz in Namespace Public or 1151: A Conflict Exists with Definition xxx in Namespace Internal. Vector In ActionScript, a Vector class object is an Array that contains a single class (base) type, and that base type is declared when the vector is defined. For example: var v:Vector.<Sprite> = new Vector.<Sprite>();
Vector where the Vector’s base type is Sprite. Strictly speaking, this Vector should contain only Sprite instances. But any data type that can be cast as a Sprite (for example, MovieClip) could be added to v. The only advantage of a Vector over an Array is that accessing Vector elements is faster than accessing Array elements. In most scenarios that benefit isn’t significant. But, when you’re accessing large numbers of elements or accessing the elements many times, you may see a significant benefit. I’ll cover this benefit in more detail in Chapter 7. 79
This page intentionally left blank
Chapter 5 Using the Flash API and Starting a Flash Game This chapter discusses how to use the Flash Application Programming Interface (API). In the context of OOP languages, an API is a list of all the classes available for your use, as well as properties, methods, and events available to the class and class members. Learning how to use APIs is key to learning and using everything available in not only Flash, but also third-party libraries such as Facebook applications, Google Web Services, 3D engines, physics engines, and many more. In fact, to use libraries as diverse as Away3D, Google Web Services, and Facebook, the two main things you need are the information in this chapter on how to use APIs and a quick-start guide or a sample file showing how to initialize each library. That won’t cover everything you need to know, but it will cover most of what you need. You’ll still need experience and/or help to do everything possible with each API, but I’ll cover how to get that extra help, too. The only way I know to gain experience is to keep trying. Don’t quit. If you can think logically and you keep trying and keep learning, you’ll gain the experience you need to be an expert. Step 1 Create a new FLA, save it in a new directory, assign a document class name Main, and open Main.as in Flash. 81
82 Chapter 5 n Using the Flash API and Starting a Flash Game Main.as package { import flash.display.MovieClip; public class Main extends MovieClip { public function Main() { } } } You’re ready to start working with the one API everyone using Flash should know how to use: the Flash API. Click Help > Flash Help > ActionScript > ActionScript 3.0 Reference > Show Packages and Classes List. Bookmark that page for easy access. (See Figure 5.1.) Figure 5.1 The initial page of the ActionScript 3.0 API. Source: Adobe Systems Incorporated. At the time of this writing, help.adobe.com/en_US/FlashPlatform/reference/actionscript/ 3/index.html is the correct link. But that may change, so if it doesn’t work, follow the steps that start with clicking Help in your Flash Pro program. Figure 5.1 shows the ActionScript 3.0 API. At the top left are all the Flash packages (directories, folders), which are collections of the Flash class files. For example, the Flash.display package contains all the DisplayObject classes, and the Flash.text
Step 1 package contains all the non-TLF text-related classes. Below the Flash packages is a list of all Flash classes (see Figure 5.2). List of classes organized by package name List of classes organized by class name Figure 5.2 If you don’t see the Classes and Packages panels, click Show Packages and Classes List. Source: Adobe Systems Incorporated. Both the upper and lower panels on the left lead to the same set of classes. The two panels are just different ways to present the same class listing. At the top, the classes are organized by packages with the packages listed alphabetically. At the bottom, the classes are organized in alphabetical order. I always find the lower (class-based) panel listing to be the quickest way to find a particular class. Knowing which class to search isn’t always obvious, but when you do know which class you want to check, you can quickly see all the available properties, methods, and events and determine whether that class will meet your needs. In addition, there is often sample code that shows how to use some of the class properties, methods, and/or events. At the top left of the page, in the Package and Class Filters section, there is a Runtimes link and a Products link. Mouse over each if you want to filter the class list by a particular Flash Player version and Adobe product (for example, Flex versus Flash). But even with the most restrictive filtering, the list of classes is overwhelming. Don’t worry about the list’s length. You don’t need to be familiar with all the classes because you’ll probably use only a small subset of them in any game. 83
84 Chapter 5 n Using the Flash API and Starting a Flash Game Because the Main class you created extends the MovieClip class, start with the MovieClip class. Scroll to MovieClip in the Classes panel and click that link. First, notice that at the very top, the MovieClip package is listed (see Figure 5.3). Figure 5.3 There’s a lot of useful information at the top of each class, before you even get to the core of each class listing—the list of properties, methods, and events available to that class. Source: Adobe Systems Incorporated. You’ll use this information whenever you see a 1046: Type Was Not Found or Was Not a Compile-Time Constant: xxxx compile-time error. You’ll look up the xxxx class, check the package, and use an import statement to remedy the error. If xxxx=MovieClip, that would be: Import flash.Display.MovieClip; Leave the Adobe help file open to the MovieClip class and go back to your FLA. Get ready to unleash your inner artist, because we’re finally going to start making a game.
Step 1 First, a word of warning about what follows. We’re going to create a basic game with one player tank (that you control) that tries to shoot one computer-controlled enemy tank. This will be a top-down tank combat game. I’ll go through the steps as if I haven’t made this game yet (which is true), with missteps and all. I’m presenting it this way to show you how games and coding evolve and, most of all, how to solve problems you’ll encounter in real-world coding. Some of the errors and missteps I make are intentional—they’re things that I would’ve done or mistakes I’ve seen others make. Some of the errors are the result of me coding like I usually do— and making mistakes like I usually do. I don’t want to waste your time, so for the next few pages, you should probably just read and not spend much (or any) time working on creating these class files unless you find something interesting and you want to test it. Start by drawing one tank. Don’t waste time making it fancy; just create a new MovieClip. Choose Insert > New Symbol and assign it a name (for example, _player tank mc). Select MovieClip from the Type combo box and check Export for ActionScript. In the Class field, type PlayerTank and then click OK. Notice that Flash will fill the Base Class field with flash.display.MovieClip. This indicates that PlayerTank extends the MovieClip class. The PlayerTank class (that Flash created for you) can now use all the properties, methods, and events of the MovieClip class. Check the MovieClip API to see what’s available. You don’t need to learn how to use all the properties, methods, and events, but you should glance over the list so you have an idea of what you can do with the MovieClip class. You should be in Symbol editing mode for _player tank mc. Add graphics to _player tank mc using the Flash drawing tools. A small 30 × 40 rectangle will work for now, or you can make it a little fancier. But keep its size about 30 × 40 pixels. Almost all of your objects should have the same registration point so you can consistently position your objects without wasting time checking each one’s registration point. I prefer using the 0,0 registration point for all MovieClips except those I know I’ll want to rotate around their center, like this tank. So, center the tank relative to its registration point. That is, half the tank should be to the left of 0 and half should be above 0. Right-click _player tank mc in your Library panel and click Edit Class. You should see (or you’ll need to create) the following code. 85
86 Chapter 5 n Using the Flash API and Starting a Flash Game PlayerTank.as package { import flash.display.MovieClip; // Flash knows PlayerTank must extend MovieClip. We can use // all the properties, methods and events available to the // MovieClip class in our PlayerTank class. public class PlayerTank extends MovieClip { public function PlayerTank() { // constructor code // this constructor function executes whenever // a _player tank mc symbol is dragged from // the library to the stage and whenever // code like: var pt:PlayerTank = new PlayerTank(); // executes in another scope. But you should not use the // new constructor for PlayerTank here or you will probably crash // Flash. Think about what kind of loop that would create. } } } Save this PlayerTank class file as PlayerTank.as. Follow the same steps to create an _enemy tank mc with class EnemyTank but make that MovieClip a different color or do something else so you can distinguish the two MovieClips. The EnemyTank class code should look exactly like the PlayerTank class code (at this point), except Player is replaced by Enemy in both the code and the file name. The user (you) will control the player tank using the keyboard. The idea I want to convey is how a game evolves from an idea to code and how the code is almost never written by starting and finishing one class, then starting and finishing another class, and so on until you’ve coded the last class. Typically, you will create a few classes to start, and each will contain almost no code except some trace() functions for testing. Then, you’ll gradually add code to one class, and then another class, and so on. Despite adding code to various classes one after the other, you probably won’t finish coding in each class at about the same time. You might complete some classes long before others. The important point here is that there are neither rules nor best practices about how your game should evolve or how much coding you should do in one class before adding/editing code in another class. There are guidelines for what should be in each class, and we’ll cover one of those later, after seeing why that guideline is helpful. The lack of rules or best practices may seem unsettling, but it also offers flexibility.
Version 0a First, we’ll make a player tank instance and an enemy instance in Main and then test the initial version of this game. We won’t even number these first starts because they show more about what you shouldn’t do than about what you should do. Version 0a Main.as package { import flash.display.MovieClip; public class Main extends MovieClip { public function Main() { // create a PlayerTank instance using the “new” constructor var player:PlayerTank = new PlayerTank(); // add player to the display list addChild(player); // create an EnemyTank instance var enemy:EnemyTank = new EnemyTank(); // add enemy to the display list addChild(enemy); } } } If you’re unfamiliar with ActionScript, you can find information about what I just coded using the Flash API and looking at the MovieClip class. The only code I used was the new constuctor and addChild(). Because Main extends the MovieClip class, it can use the addChild() method of the MovieClip class. Remember, if you see an error message that you don’t understand or know how to resolve, check Appendix A, “Errors That Trigger an Error Message,” after checking the Flash appendixes. I don’t know about you, but I triggered two errors already because I forgot to save the EnemyTank class file. Remember, making the class isn’t enough. You have to save it for the code to be compiled into a SWF. Changes made to an unsaved class file won’t be incorporated into the test SWF. If you test, you will test old code in the previously saved class file(s). After an error-free test, you should see enemy at the top left of the stage, with half of enemy to the left of the stage and half of enemy above the stage. If you don’t see player, use the trace() function to determine whether player is on-stage. Hint: All DisplayObjects have a non-null stage property if they are on-stage. trace(player.stage); // [object Stage] if on-stage, else null 87
88 Chapter 5 n Using the Flash API and Starting a Flash Game After confirming player is on-stage, check player’s x and y properties to confirm that it’s at the same location as enemy, which explains why we don’t see player—it’s beneath enemy. Because addChild adds a child above all other children already in the parent, the first child added will be beneath the second added child. Because we didn’t assign an x or y property for either player or enemy, they are both at 0,0, the default location when those properties aren’t assigned. More precisely, their registration points are at 0,0. We’ll fix that so those two MovieClips aren’t on top of each other and add keyboard controls so we can control player. If you have no clue what class you should use to add keyboard controls, don’t worry. This is where you start learning to use the Flash API. You should have the API open in your browser. Step 2 Check for something obvious that you can use—say, a Keyboard class or something similar. Scroll down the Flash API to find the Keyboard class and see whether it meets your needs (detecting when a keyboard key is pressed and determining which key is pressed). Nearby, you might see the KeyboardType class and think that looks good. Click the KeyboardType link and read to see whether you think it might work. If you do, you’ll see that the KeyboardType class isn’t about keyboard key typing. It has something to do with different types of keyboards or if no keyboard is available. That won’t work for our needs, so click the Keyboard class. Now, that looks good. Sometimes the explanatory text about a class can be like reading Aramaic. But sometimes the explanation is clear, like the Keyboard class’s explanation. For most classes, you’ll use the class constructor, but for the Keyboard class there is no constructor and no explanation of how to use the class. That’s no problem; scroll down past the list of events and look for sample code. If you’re like me, finding sample code is a major bonus. I often find it easier to copy and paste sample code and then tweak it to suit my needs than to figure out how to write code by reading the properties, methods, and events. The first sample code is about a virtual keyboard that looks unhelpful, so keep scrolling. I see nothing useful. Go back to the top of the Keyboard class and see whether there’s a link to something useful or whether you’ll need to look for another likely class. (See Figure 5.4.) Actually, both looking for a link and checking for another class will work. You can click the Capturing Keyboard Input link at the top of the Keyboard class, just below
Step 2 Helpful-looking link Figure 5.4 Top of Keyboard class listing. Source: Adobe Systems Incorporated. Another helpfullooking link Figure 5.5 Top of KeyboardEvent class listing. Source: Adobe Systems Incorporated. More Examples, to see an example of how to use the Keyboard class (see Figure 5.4) or look at the only other likely helpful class—the KeyboardEvent class, which has a good explanation about adding a KeyboardEvent listener to the stage and has sample code. (See Figure 5.5.) This is a good spot to open another FLA and test keyboard-handling code to make sure you understand how to capture keyboard input. 89
90 Chapter 5 n Using the Flash API and Starting a Flash Game In addition to capturing keyboard input to control the PlayerTank instance, we want to position both tanks so they are entirely on-stage. That means we want their x properties to be at least half their width and their y properties to be at least half their height. The maximum x and y depend on the stage size. To determine the stage size, use the API to check the Stage class. For the Stage class, there is a width, a height, a stageWidth, and a stageHeight. Reading about those properties helps, but it’s still confusing. So, open a new FLA, add the following to the Actions panel: trace(stage.width,stage.stageWidth); and test. You can see zero and the actual width of the stage that is listed in the Properties panel. That tells us that we want to use stage.stageWidth and stage.stageHeight, but we want to understand what stage.width is, so draw a few things on-stage (and on the pasteboard). Now the help file information will make sense, and you can understand how both work. We still want to use stage.stageWidth, though. returns the width of the DisplayObjects on- and off-stage when it executes, and stage.stageWidth returns the width of the stage displayed in the Properties panel when the stage is selected. If you still don’t understand stage.width, open Flash and test until you understand. stage.width We want to position the tanks using some randomness. This is where you need experience or help because it’s not obvious what class to look for when you want to add an element of randomness using ActionScript. That is why I mentioned Math.random() in Chapter 4. We need to use the Math class. So, let’s add that keyboard event listener, position the two tanks, and test the code. Version 0b Main.as package { import flash.display.MovieClip; import flash.events.KeyboardEvent; public class Main extends MovieClip { public function Main() { var player:PlayerTank = new PlayerTank(); positionF(player); var enemy:EnemyTank = new EnemyTank(); positionF(enemy); // Main is our document class, so we don’t need
Version 0c // to use an Event.ADDED_TO_STAGE listener to // ensure the stage property is available stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownF); } // The code in positionF() positions mc completely // on-stage if mc has registration point at its center. // I created a new fla to test the code when Math.random() // was 0 and when it was 1 to ensure I coded this // correctly. And again, if you know nothing about // ActionScript and want to position the tanks on-stage, // check the API. For all DisplayObjects including // MovieClips it lists x, y, width, and height properties // you can use. private function positionF(mc:MovieClip):void { addChild(mc); mc.x = int(mc.width/2+(stage.stageWidth-mc.width)*Math.random()); mc.y = int(mc.height/2+(stage.stageHeightmc.height)*Math.random()); } private function keyDownF(e:KeyboardEvent):void { trace(e); } } } Now, we need to determine which keys will move player forward and back and which keys will turn player to the left and right. We could check the KeyboardEvent class and use the constants that correspond to certain keys, but it’s easier to just use the trace(e) shown; click the left arrow, up arrow, right arrow, and down arrow; and note the keyCode properties traced (37,38,39,40, respectively). So, here’s the next version of way we want. Main that confirms we have our key codes defined the Version 0c Main.as package { import flash.display.MovieClip; import flash.events.KeyboardEvent; public class Main extends MovieClip { public function Main() { var player:PlayerTank = new PlayerTank(); positionF(player); 91
92 Chapter 5 n Using the Flash API and Starting a Flash Game var enemy:EnemyTank = new EnemyTank(); positionF(enemy); stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownF); } private function positionF(mc:MovieClip):void { addChild(mc); mc.x = int(mc.width/2+(stage.stageWidth-mc.width)*Math.random()); mc.y = int(mc.height/2+(stage.stageHeightmc.height)*Math.random()); } private function keyDownF(e:KeyboardEvent):void{ switch(e.keyCode){ case 37: trace(“L”); break; case 38: trace(“U”); break; case 39: trace(“R”); break; case 40: trace(“D”); break; } } } } We can test that and notice the keyboard keys are working, but you’ll also find that the tanks can overlap, and we don’t want to allow that. So, let’s work on the two tanks’ initial positioning so that if they overlap, one is repositioned. Make sure your stage is at least three times wider than player.width plus enemy.width and three times higher than player.height plus enemy.height so this code doesn’t create an endless loop. There is no overlap property or method, but if you search the MovieClip API, you’ll find the hitTestPoint() and hitTestObject() methods that should look promising. After reading the API about those two methods, you should see that hitTestObject() looks as if it will work for checking when one MovieClip overlaps (or hits) another MovieClip.
Version 0d If you don’t find anything that looks helpful (and that’s easy to do with so many properties and methods listed), use a search engine. Searching for “Flash as3 overlap test” should focus your attention on hitTestObject() and hitTestPoint(). And, if you fail to find the needed property, method, or event by scanning the API and using a search engine, you can use the appropriate Flash forum for help. Chapter 1 lists the links. Also, we want to replace those keyDownF() traces with code that moves and rotates player, and we want to make sure player cannot be moved off-stage. Checking the MovieClip API reveals there is a rotation property we can use to rotate the tanks. Here is the next version of the Main class. Version 0d Main.as package { import flash.display.MovieClip; import flash.events.KeyboardEvent; public class Main extends MovieClip { private var speed:int = 2; private var rotationRate:int = 3; public function Main() { var player:PlayerTank = new PlayerTank(); positionF(player); var enemy:EnemyTank = new EnemyTank(); positionF(enemy); while(player.hitTestObject(enemy)){ positionF(enemy); } stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownF); } private function positionF(mc:MovieClip):void { addChild(mc); mc.x = int(mc.width/2+(stage.stageWidth-mc.width)*Math.random()); mc.y = int(mc.height/2+(stage.stageHeightmc.height)*Math.random()); } private function keyDownF(e:KeyboardEvent):void{ switch(e.keyCode){ case 37: trace(“L”); player.rotation -= rotationRate; 93
94 Chapter 5 n Using the Flash API and Starting a Flash Game case 38: trace(“F”); // Move player forward in the same direction as // player’s rotation player.x += Math.cos(player.rotation*Math.PI/180); player.y += Math.sin(player.rotation*Math.PI/180); break; case 39: trace(“R”); player.rotation += rotationRate; break; case 40: trace(“B”); // Move player backward player.x += Math.cos(-player.rotation*Math.PI/180); player.y += Math.sin(-player.rotation*Math.PI/180); break; } boundaryCheckF(); } private function boundaryCheckF():void{ if(player.x<player.width/2){ player.x = player.width/2; } else if(player.x>stage.stageWidth-player.width/2){ player.x = stage.stageWidth-player.width/2; } if(player.y<player.height/2){ player.y = player.height/2; } else if(player.y>stage.stageHeight-player.height/2){ player.y = stage.stageHeight-player.height/2; } } } } Test this, and you’ll promptly get many 1120: Access of Undefined Property xxxx errors. This is one of the errors discussed previously and in Appendix A. We made player and enemy local to the constructor Main(). We should remedy that by declaring those variables outside the constructor (#4 in the list at the start of Chapter 3 about class file structure). Once that is done, you’ll find all sorts of problems. First, unless your _tank mc is heading toward the positive x-axis, your tank won’t appear to be moving forward when
Version 0d the up arrow is pressed. Fix that by making sure the front of your tank is heading to the right (0 degrees and radians) and then test. Second, the left arrow causes the tank to rotate in a very strange manner. Fortunately, we left our L,U,R,D traces during this test, so it’s obvious that when we press the left arrow (and see L,F,L,F,L,F…), both case 37 and case 38 are executing. Checking our code, you can see that we omitted a break; at the end of case 37. So, let’s fix that and test. Now you’ll notice that our tank doesn’t go back correctly. A check of our code will reveal faulty logic. When the back key is pressed, we don’t want our tank to move along the opposite angle (-angle). We want it to move in the opposite direction. Fix that and test. Everything works the way it should, but there’s a major problem. Actually, there is more than one major problem, and there are some minor problems, but I’ll address the biggest problem first: The Main class is quickly becoming a mess. We have all this code for controlling player in Main, and we’ll need more code, which will make Main an even bigger mess. In addition, we’ll need to add an introduction view (by view, I mean what is presented on-stage) to give some information about the game, and we should probably allow users to customize the keys they use for movement. Not everyone likes using the arrow keys. (Heck, I don’t like using the arrow keys.) In addition, we’ll need a game-over view eventually. We’re heading for a major mess in Main. So, while we can continue to put all of our coding in Main, that’s just like putting all of our code in frame 1 of the main timeline, and that bypasses one of the biggest advantages of OOP: encapsulation. By encapsulation, I mean that each class should contain only the information it needs to do its job. We want our Main class to control which views are presented on-stage. When the game starts, Main will add an intro view. When the user is finished with the intro view, Main will remove the intro view and add the game view (where the player and enemy tanks will fight), and when the game is over, Main will remove the game view and add the game over view. We’ll use an IntroView class to control what happens in the introduction view, and we’ll use a GameView class to monitor what happens when player and enemy fight and an EndView to control what is displayed when combat is over—maybe the user’s score or a taunting message. 95
96 Chapter 5 n Using the Flash API and Starting a Flash Game We don’t want the GameView class to control the fighting. We want all the code that controls player to be in the PlayerTank class and all the code that controls enemy to be in the EnemyTank class. That will simplify Main so it’s easier to work with, will provide better encapsulation, and will make it easier and faster to find the code we need to edit when changes and additions are needed. While we’re simplifying the code, we’ll create a keyboard controls class (KBcontrols) to store the key codes for moving left, right, up, and down. We’ll use static methods so we can assign those key codes in IntroView and use them in PlayerTank. We don’t want to be forced to pass those key codes back and forth, because we’re going to have one PlayerTank instance in IntroView (so users can test the keyboard controls) and a different PlayerTank instance in GameView. In addition, we might later want to allow users to redefine the keyboard controls while in GameView or elsewhere. We could make KBcontrols a singleton class, but I think we should make it a class with static properties. Actually, we already used a singleton class in Chapter 4 for a shape-based hit test in the BitmapData section. So, showing the other way to make data easily available among several classes is reasonable. We could add quite a few things (such as scoring, a timer, enemy intelligence, and so on), and we don’t know exactly what classes we’re going to have at the end of this project, and we sure don’t know all the code that will be in each class that we may need. That may be a little unsettling, but it’s how applications and games evolve (at least, when I’m the developer). While we’re getting this project properly organized, let’s put the class files in their own directory so we don’t end up with a mess of files all in one directory. By convention, class files created by someone with website www.xyz.com should be placed in an xyz subdirectory of a com directory. Because my programming website is www. kglad.com, I’m going to put these class files in com/kglad. If you recall from Chapter 3, that means my class files will use the com.kglad package designation. It also means that any object in the library (such as _player tank mc) will need its class renamed from PlayerTank to com.kglad.PlayerTank, and our document class Main will need to be changed to com.kglad.Main. If we fail to indicate our document class file’s location in the Properties panel, the document class won’t be used. We’ll test our FLA, and nothing will happen. We would then put a trace(“Main”) in the Main class constructor, and we’d see no output.
Version 0d That would remind us that we have a path problem with our document class and prompt us to fix the class path in the Properties panel. Likewise, if we have a class path problem when we start trying to use a class file like PlayerTank, we’ll find (using the trace() function again) that the class file isn’t being used. That would remind us to fix that library path. Finally, following is the code from the refactored classes. These classes are on this book’s companion website (www.courseptr.com/downloads). Copy the tank_combat directory from the site to a subdirectory on your hard drive so you can manipulate the files. For the remainder of this chapter, all the files mentioned will be referencing your hard drive files. In tank_combat, open tank_combat_01.fla in your Flash Pro. In the com.kglad subdirectory, open Main_01.as, IntroView_01.as, KBcontrol_01.as, and PlayerTank_01.as. Save those four pseudo class files as Main.as, IntroView.as, KBcontrol.as, and PlayerTank.as, respectively. You should now have the four classes (Main, IntroView, KBControl, and PlayerTank) open in Flash Pro (or your preferred class-editing software) where they’re easy to read and test. I refer to the ClassName_01.as files as pseudo class files because they aren’t true class files. If they were, they would need to define a ClassName_01 class. For example, if you changed the document class in the FLA to use Main_01, you should see a 5008 error message because a class file named Main_01 should be defining a Main_01 class, not a Main class. I created those pseudo classes to make it easy for you to follow the discussion. I don’t typically make pseudo classes like that when I code. After you save each of those four pseudo classes with the correct class name, there shouldn’t be any 5008 errors, and you should have four proper classes. Don’t worry if those four classes already exist. Overwrite them. You’ll be using the various pseudo class file versions to create different versions of Main, IntroView, KBcontrol, and PlayerTank as you read and as this game evolves. The code displayed in your Flash Pro should match the following code, though you may find additional comments here. The comments provide additional information that you should read if you have any questions about the code. You should test this code (after saving those classes) by clicking Test > Flash Movie > in Flash Professional. 97
98 Chapter 5 n Using the Flash API and Starting a Flash Game Version _01 Version _01 Main.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; // Because Main is in the com.kglad package, // I don’t need import other com.kglad // package classes like IntroView public class Main extends MovieClip { // To record the current view for possible // later use when this class might be adding // and removing more than intro, game and end-game views private var currentView:MovieClip; public function Main() { // this constructor is called as soon as the // game starts and the first thing I do is // add an IntroView instance in addIntroViewF(); addIntroViewF(); } private function addIntroViewF():void{ // here introView is created (but this // reference is local) var introView:IntroView = new IntroView(); // I’m adding a listener to check if and // when the start game button is clicked. // That button has its listener defined in // IntroView and, when clicked, dispatches // an event, “startgameE”. introView.addEventListener(“startgameE”,addGameViewF); // I’m saving the current view currentView = introView; // add introView to the display addChild(introView); } private function addGameViewF(e:Event):void{ // nothing here yet, but this will be where // I create a GameView } } } The _01 version of the IntroView class follows. It is the class associated with the library MovieClip symbol intro view.
Version _01 Version _01 IntroView.as package com.kglad { // import the needed classes import flash.display.MovieClip; import flash.events.MouseEvent; import flash.events.KeyboardEvent; import flash.events.Event; public class IntroView extends MovieClip { public function IntroView() { // Wait until this is added to the display list, // though in this particular class I do not need // this. But it’s a good habit to use it for // display objects other than the document class. this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ // Define all the click listeners listenersF(); // Add a PlayerTank instance to introView so // users can test the keyboard controls addPlayerTankF(); } // Define the click listeners private function listenersF():void{ left_mc.addEventListener(MouseEvent.CLICK,leftF); // Because I used MovieClips for my buttons, // they don’t act like buttons. The mouse does // not respond like it is over a button. // I need to find a MovieClip property that will // make my MovieClip buttons act like buttons. // The buttonMode property is promising. So is // useHandCursor, but after testing, buttonMode // is the property that should be used. left_mc.buttonMode = true; right_mc.addEventListener(MouseEvent.CLICK,rightF); right_mc.buttonMode = true; forward_mc.addEventListener(MouseEvent.CLICK,forwardF); forward_mc.buttonMode = true; back_mc.addEventListener(MouseEvent.CLICK,backF); back_mc.buttonMode = true; startgame_mc.addEventListener(MouseEvent.CLICK,startgameF); startgame_mc.buttonMode = true; } 99
100 Chapter 5 n Using the Flash API and Starting a Flash Game private function addPlayerTankF():void{ // playerBG_mc is a MovieClip already added to // the intro view library MovieClip used to // restrict player’s movement var w:int = playerBG_mc.width; var h:int = playerBG_mc.height // Create a player instance passing w and h // used to restrict player’s movement. // Movement restriction done in PlayerTank. var player:PlayerTank = new PlayerTank(w,h); positionF(playerBG_mc,player); } private function positionF(parent_mc:MovieClip,mc:MovieClip):void { parent_mc.addChild(mc); // Position player in the middle of playerBG_mc mc.x = int(parent_mc.width/2); mc.y = int(parent_mc.height/2); } // Click listener functions used to define keydown // listeners. That is, after the user clicks left_mc, // the next keyboard key pressed will be used as // player’s left control key. private function leftF(e:MouseEvent):void{ stage.addEventListener(KeyboardEvent.KEY_DOWN,leftKeyF); } private function rightF(e:MouseEvent):void{ stage.addEventListener(KeyboardEvent.KEY_DOWN,rightKeyF); } private function forwardF(e:MouseEvent):void{ stage.addEventListener(KeyboardEvent.KEY_DOWN,forwardKeyF); } private function backF(e:MouseEvent):void{ stage.addEventListener(KeyboardEvent.KEY_DOWN,backKeyF); } // Finally, key listeners to assign key codes in KBcontrols private function leftKeyF(e:KeyboardEvent):void{ // Because leftKeyCode is a static setter, it’s // used like this: KBcontrols.leftKeyCode = e.keyCode; // remove the keydown listener stage.removeEventListener(KeyboardEvent.KEY_DOWN,leftKeyF); } private function rightKeyF(e:KeyboardEvent):void{ KBcontrols.rightKeyCode = e.keyCode;
Version _01 stage.removeEventListener(KeyboardEvent.KEY_DOWN,rightKeyF); } private function forwardKeyF(e:KeyboardEvent):void{ KBcontrols.forwardKeyCode = e.keyCode; stage.removeEventListener(KeyboardEvent.KEY_DOWN, forwardKeyF); } private function backKeyF(e:KeyboardEvent):void{ KBcontrols.backKeyCode = e.keyCode; stage.removeEventListener(KeyboardEvent.KEY_DOWN,backKeyF); } private function startgameF(e:MouseEvent):void{ // The “startgameE” event is dispatched to // all IntroView instances that have a // “startgameE” listener. (The only instance // created is in Main and it has this listener.) dispatchEvent(new Event(“startgameE”)); } } } Here is the _01 version of the trol PlayerTank instances. KBcontrol class that is used to store the keys that con- Version _01 KBcontrols.as package com.kglad { public class KBcontrols{ // variable declarations and default key codes private static var _leftKeyCode:int = 37; private static var _rightKeyCode:int = 39; private static var _forwardKeyCode:int = 38; private static var _backKeyCode:int = 40; public function KBcontrols() { // constructor code // There’s nothing here because there // will be no KBcontrol instances. This // class is used by other classes to store // and retrieve key codes. } // static getters public static function get leftKeyCode():int{ return _leftKeyCode; } public static function get rightKeyCode():int{ 101
102 Chapter 5 n Using the Flash API and Starting a Flash Game return _rightKeyCode; } public static function get forwardKeyCode():int{ return _forwardKeyCode; } public static function get backKeyCode():int{ return _backKeyCode; } // static setters with some (but, at this point, // not much) error checking public static function set leftKeyCode(n:int):void{ if(keyCodeF(n)){ _leftKeyCode = n; } } public static function set rightKeyCode(n:int):void{ if(keyCodeF(n)){ _rightKeyCode = n; } } public static function set forwardKeyCode(n:int):void{ if(keyCodeF(n)){ _forwardKeyCode = n; } } public static function set backKeyCode(n:int):void{ if(keyCodeF(n)){ _backKeyCode = n; } } private static function keyCodeF(n:int):Boolean{ if(n>=37&&n<=105){ return true; } else { return false; } } } } And finally, here is the _01 version of the Version _01 PlayerTank.as package com.kglad { PlayerTank class:
Version _01 // again, import the needed classes import flash.display.MovieClip; import flash.events.Event; import flash.events.KeyboardEvent; public class PlayerTank extends MovieClip { // Define a variable, speed, used to control // speed of movement. private var speed:int = 3; // Define a variable, rotationRate, to control // the rate of rotation. private var rotationRate:int = 3; // These are movement limits to keep instances // within a confined rectangle. In introView // this is defined using playerBG_mc and when we // create a GameView instance these limits will // be defined by stage’s stageWidth and // stageHeight property. Or, even better, // I can put a display at the top of the GameView // instance which will display the score and time // and I’ll restrict movement using a game view // rectangle similar to the one used in intro view // so it doesn’t interfere with the score/time display private var xLimit:int; private var yLimit:int; public function PlayerTank(_xLimit:int,_yLimit:int) { // The movement limits are passed in the constructor // and used in the below moveLimitsF() xLimit = _xLimit; yLimit = _yLimit; // The usual listener needed to ensure this is // added to the stage before executing code that // depends on display list objects. In PlayerTank, // this listener is needed or I’ll trigger a 1009 null // object reference when I try to reference “stage”. this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ // Listener for keydown events stage.addEventListener(KeyboardEvent.KEY_DOWN,keydownF); } // Keydown listener function that checks if the pressed key // matches a control key and then updates the PlayerTank // instance’s x and y and rotation properties private function keydownF(e:KeyboardEvent):void{ 103
104 Chapter 5 n Using the Flash API and Starting a Flash Game if(KBcontrols.leftKeyCode==e.keyCode){ this.rotation -= rotationRate; } if(KBcontrols.rightKeyCode==e.keyCode){ this.rotation += rotationRate; } if(KBcontrols.forwardKeyCode==e.keyCode){ this.x += speed*Math.cos(this.rotation*Math.PI/180); this.y += speed*Math.sin(this.rotation*Math.PI/180); } if(KBcontrols.backKeyCode==e.keyCode){ this.x -= speed*Math.cos(this.rotation*Math.PI/180); this.y -= speed*Math.sin(this.rotation*Math.PI/180); } // Check if a move limit has been exceeded. moveLimitsF(); } private function moveLimitsF():void{ if(this.x<this.width/2){ this.x = this.width/2; }else if(this.x>xLimit-this.width/2){ this.x = xLimit-this.width/2; } if(this.y<this.height/2){ this.y = this.height/2; } else if(this.y>yLimit-this.height/2){ this.y = yLimit-this.height/2; } } } } To test this code, open tank_combat_01.fla in Flash Pro and click Control > Test Movie > in Flash Professional. Test the movement of the PlayerTank instance. It’s terrible. When you press and hold one of the controls, the character moves a little and then there’s a delay followed by steady stuttering movement. On a positive note, the buttons used to define the custom move and rotation keys work well. But the stuttering movement is a major problem. We can smooth out some of the stuttering movement by increasing the FLA’s frame rate in the Properties panel, but that initial delay after pressing and holding the control key is due to the delay in the keyboard repeat delay. And the frequency of calls to keydownF() is dependent on
Version _01 the keyboard’s repeat rate. Neither the repeat delay nor the repeat rate are controllable using ActionScript. So, we cannot use a keyboard listener to trigger smooth movement. We can use the keyboard listener to detect key presses, but for movement we should use an enterFrame or Timer loop. (See the Chapter 4 section about loops.) I’m going to try an enterFrame loop. And here’s another benefit of encapsulation: We don’t have to wade through so much code to find the relevant code that needs to be edited. The code used to control player’s movement and rotation is in PlayerTank.as. This code has advanced to a more typical starting point for an experienced developer. The initial versions displayed common approaches to coding taken by beginning coders and displayed the major issue with putting all your code in the document class. I will no longer discuss how to use the Flash API. I don’t expect that you are familiar with all or even most of it. But you should now know how to use the API to find what you need and how to use other resources when you cannot find what you need in the API. You will probably need the Flash API open to reference when creating a Flash game. I know that I always have to open it, sooner or later, for any major project. Using other APIs is very similar to using the Flash API. I’ll discuss some other APIs in subsequent chapters. Discussion of the tank combat game is ready to advance to the next chapter, where we’ll continue to develop the game, again showing one way to develop a game and how to debug the problems that occur during development. 105
This page intentionally left blank
Chapter 6 Developing a Flash Game We ended the previous chapter with version _01 of a tank combat game. In this chapter, we’ll continue development of that game. The main purpose of this chapter is to show you how a game typically evolves. Well, I don’t really know if that previous sentence is true. What I do know is that this chapter will show you how a game I develop typically evolves. That means there will be problems, errors, and missteps that need to be handled and debugged. I’m going to point those out between game versions and show you one way to remedy them, but I will no longer explicitly discuss using the ActionScript 3.0 API Language Reference (previously and henceforth called the Flash API). I used the Flash API repeatedly to code these game versions, and I expect you will need to use it repeatedly to follow this code, but you should now know how (see Chapter 5, “Using the Flash API and Starting a Flash Game”) and when (each and every time you encounter code with which you aren’t familiar) to use it. For the next update, open PlayerTank_02.as and save as PlayerTank.as. None of the other classes has any changes. If you compare the _01 and _02 file sizes, you can see all the classes are the same except PlayerTank. Nevertheless, I created _02 versions of all four class files to make it easy to keep track of which classes are used with which game version. You can open all the _02 pseudo class files and save them as proper class files or just leave the previously created Main, IntroView, and KBcontrol intact. 107
108 Chapter 6 n Developing a Flash Game I also made an _02 version of the FLA, which contains changes in _tank mc. I set up _tank mc so I could use it as a player tank and as an enemy tank by applying color to the base and turret top of the tanks to distinguish them. I did that to give me flexibility if I want to create different types of enemy tanks, perhaps with different speed and rotation rate and different point values once I start adding scoring. Here is version _02 of PlayerTank. Version _02 Version _02 PlayerTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.KeyboardEvent; public class PlayerTank extends MovieClip { private var speed:int = 3; private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; private var directionS:String; public function PlayerTank(_xLimit:int,_yLimit:int) { xLimit = _xLimit; yLimit = _yLimit; this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ // Listener for keydown events to start an enterframe loop stage.addEventListener(KeyboardEvent.KEY_DOWN,keydownF); // Listener for keyup events to stop the enterframe loop stage.addEventListener(KeyboardEvent.KEY_UP,keyupF); } private function keydownF(e:KeyboardEvent):void{ // Add/start enterframe loop this.addEventListener(Event.ENTER_FRAME,enterframeF); if(KBcontrols.forwardKeyCode==e.keyCode){ directionS = "forward"; } if(KBcontrols.backKeyCode==e.keyCode){ directionS = "back"; } if(KBcontrols.leftKeyCode==e.keyCode){ directionS = "left";
Version _02 } if(KBcontrols.rightKeyCode==e.keyCode){ directionS = "right"; } } private function keyupF(e:KeyboardEvent):void{ // Remove/end enterframe loop if(this.hasEventListener(Event.ENTER_FRAME)){ this.removeEventListener(Event.ENTER_FRAME,enterframeF); } } private function enterframeF(e:Event):void{ switch(directionS){ case "forward": this.x += speed*Math.cos(this.rotation*Math.PI/180); this.y += speed*Math.sin(this.rotation*Math.PI/180); break; case "back": this.x -= speed*Math.cos(this.rotation*Math.PI/180); this.y -= speed*Math.sin(this.rotation*Math.PI/180); break; case "left": this.rotation -= rotationRate; break; case "right": this.rotation += rotationRate; break } moveLimitsF(); } private function moveLimitsF():void{ if(this.x<this.width/2){ this.x = this.width/2; } else if(this.x>xLimit-this.width/2){ this.x = xLimit-this.width/2; } if(this.y<this.height/2){ this.y = this.height/2; } else if(this.y>yLimit-this.height/2){ this.y = yLimit-this.height/2; } } } } 109
110 Chapter 6 n Developing a Flash Game If you test version _02, you will see movement is much improved. There is no longer a delay between pressing a key and the tank responding. But if you press another key while one key is pressed, or if you press two keys at the same time, the tank only responds to one key. And, if you key up on one key while another is pressed, the enterFrame loop stops. The failure to respond to key-press combinations (such as forward and left) is because the code stores only the last pressed key in a string, directionS. We’ll need to use an array to store more than one key press at any one time. You should check the Array class in the Flash API to see what properties and methods are available. Tank movement stops when there is a key-up, even while another key is pressed, because we have a key-up listener function (keyupF()) that doesn’t check whether another key is pressed. It removes the enterFrame loop whenever there is a key-up and an existing loop. The tank also doesn’t stop when a boundary is encountered. We need to fix the moveLimitsF() function so it not only resets the x or y when a boundary limit is reached, but it also resets the x and y. To do that, we can store the previous x, y (before boundary check). For version _03, I updated Kbcontrols and PlayerTank. Version _03 Version _03 KBcontrols.as package com.kglad { public class KBcontrols { // These are the default arrow-key keyCodes private static var _forwardKeyCode:int = 38; private static var _backKeyCode:int = 40; private static var _leftKeyCode:int = 37; private static var _rightKeyCode:int = 39; // I’m using a static array to store the keycodes. // A keyCode’s index in _keyCodeA is crucial to determine // the tank’s response to that keyCode. // The first element (0-index) is forward, next back, then // left, then right. private static var _keyCodeA:Array = [_forwardKeyCode,_backKeyCode,_leftKeyCode,_rightKeyCode]; public function KBcontrols() { // constructor code
Version _03 } // keyCodeA is read-only; i.e., there is no setter for it. public static function get keyCodeA():Array{ return _keyCodeA; } // The 4 controls can be set but there is no need for a // getter for them. public static function set forwardKeyCode(n:int):void{ if(keyCodeF(n)){ _keyCodeA[0] = n } } public static function set backKeyCode(n:int):void{ if(keyCodeF(n)){ _keyCodeA[1] = n; } } public static function set leftKeyCode(n:int):void{ if(keyCodeF(n)){ _keyCodeA[2] = n; } } public static function set rightKeyCode(n:int):void{ if(keyCodeF(n)){ _keyCodeA[3] = n; } } private static function keyCodeF(n:int):Boolean{ if(n>=37&&n<=105){ return true; } else { return false; } } } } Version _03 PlayerTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.KeyboardEvent; public class PlayerTank extends MovieClip { private var speed:int = 3; 111
112 Chapter 6 n Developing a Flash Game private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; private var prevX:Number; private var prevY:Number; private var keyCodeIndex:int; private var directionA:Array = []; private var boundaryViolation:Boolean; public function PlayerTank(_xLimit:int,_yLimit:int) { xLimit = _xLimit; yLimit = _yLimit; this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ // Initial values for prevX and prevY, though // these aren’t needed unless this instance // starts on a boundary. prevX = this.x; prevY = this.y; stage.addEventListener(KeyboardEvent.KEY_DOWN,keydownF); stage.addEventListener(KeyboardEvent.KEY_UP,keyupF); } // I revised KBcontrols so it stores the 4 tank control // keycodes in a static array keyCodeA. // The first element (with index 0) is the forward keycode; // the 2nd, 3rd, and 4th elements are back, left, and right, resp. // Using an array (keyCodeA) allows me to avoid using for loops // to detect if a pressed key is a control key. private function keydownF(e:KeyboardEvent):void{ // Here is precisely how using keyCodeA eliminates the // need for a for loop: arrays have an indexOf property // that allows an easy-to-code way to check if a value // is an array element. // In addition, it returns the index of the found value. // Because the 0-index is the forward key and 1-index // is the back key, I can determine not only if a control // key was pressed, but exactly which control key. keyCodeIndex = KBcontrols.keyCodeA.indexOf(e.keyCode); // If keyCodeIndex==-1, e.keyCode is not in keyCodeA. // Otherwise, it is and its value is the index of // e.keyCode in keyCodeA if(keyCodeIndex>-1){ // By checking which directionA elements are true, // I can determine which control keys are
Version _03 // currently pressed. directionA[keyCodeIndex] = true; } this.addEventListener(Event.ENTER_FRAME,enterframeF); } private function keyupF(e:KeyboardEvent):void{ // Same code as above. Only in keyupF, I’m going to set // the corresponding value in directionA to false to // indicate this key is not currently being pressed. keyCodeIndex = KBcontrols.keyCodeA.indexOf(e.keyCode); if(keyCodeIndex>-1){ directionA[keyCodeIndex] = false; } // Only need and want to check for enterframe removal // if enterframe listener has been added if(this.hasEventListener(Event.ENTER_FRAME)){ // Check if any control key is pressed and, // if not, remove the enterframe loop. if(directionA.indexOf(true)==-1){ this.removeEventListener(Event.ENTER_FRAME,enterframeF); } } } private function enterframeF(e:Event):void{ // No if-else statements here now. I want to // change rotation and position if two // appropriate control keys are pressed together. if(directionA[0]){ //trace("foreward"); this.x += speed*Math.cos(this.rotation*Math.PI/180); this.y += speed*Math.sin(this.rotation*Math.PI/180); } if(directionA[1]){ //trace("back"); this.x -= speed*Math.cos(this.rotation*Math.PI/180); this.y -= speed*Math.sin(this.rotation*Math.PI/180); } if(directionA[2]){ //trace("left"); this.rotation -= rotationRate; } if(directionA[3]){ //trace("right"); this.rotation += rotationRate; 113
114 Chapter 6 n Developing a Flash Game } // Check for boundary violation. Notice prevX,prevY // are updated after moveLimitsF() executes. moveLimitsF(); prevX = this.x; prevY = this.y; } private function moveLimitsF():void{ // Check if any of the 4 boundaries are violated. // If one is, no need to check others, so if-else // appropriate here. boundaryViolation = false; if(this.x<this.width/2){ boundaryViolation = true; } else if(this.x>xLimit-this.width/2){ boundaryViolation = true; } else if(this.y<this.height/2){ boundaryViolation = true; } else if(this.y>yLimit-this.height/2){ boundaryViolation = true; } // If there is a boundary violation, reset both // x,y to the pre-violation values. if(boundaryViolation){ this.x = prevX; this.y = prevY; } } } } That is progress. No errors, and everything works pretty well. Let’s add more features. Next, we’ll add turret rotation and shooting. Thinking about that made me realize we should change turret_mc’s registration point so it’s in the center of the turret top, because we want the turret to rotate around that point. And, to make coding less complicated, we want our turret pointing toward the right, which is aligned with rotation angle zero. Those changes aren’t necessary, but they make coding easier. I tested a few ways to control turret rotation (using the mouse’s x-position and using keyboard keys) and decided those would be too difficult. There will be almost no way to beat the computer-controlled enemy tank unless aiming is easy and intuitive
Version _04 (or we code the enemy tank to do something stupid, such as aim randomly, but I’m not planning to do that). The enemy tank will have no problem moving and aiming at the same time, and using either of our first two considerations would be difficult for a human. (Well, they were difficult for me.) Therefore, I made a design decision and decided to use the cursor as an aim point. That is, the turret will rotate to aim at the cursor. So, we want to add a custom cursor that will look like a crosshair, and we need to create a projectile to shoot. For version _04, we’ll update only the PlayerTank class, and we’ll make changes to only the first part of the class. We won’t make any changes in keydownF, keyupF, enterframeF, or moveLimitsF. But we will update the FLA to add a custom cursor and redo the tank turret. Version _04 Version _04 PlayerTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.ui.Mouse; public class PlayerTank extends MovieClip { private var speed:int = 3; private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; private var prevX:Number; private var prevY:Number; private var keyCodeIndex:int; private var directionA:Array = []; private var boundaryViolation:Boolean; private var cCursor:CustomCursor; private var shellA:Array = []; private var gunL:Number; public function PlayerTank(_xLimit:int,_yLimit:int) { xLimit = _xLimit; yLimit = _yLimit; this.addEventListener(Event.ADDED_TO_STAGE,init); } 115
116 Chapter 6 n Developing a Flash Game private function init(e:Event):void{ // Initial values for prevX and prevY, though // these aren’t needed unless this instance // starts on a boundary. prevX = this.x; prevY = this.y; // Tank gun length from center of turret top to muzzle gunL = this.turret_mc.width-this.turret_mc.top_mc.width/2; // Create a custom cursor cCursor = new CustomCursor(); // Add a mouse down listener (to the tank’s parent, // which is the rectangle within which the tank // moves) to shoot this.parent.addEventListener(MouseEvent.MOUSE_DOWN,shootF); // When the mouse is over the tank’s parent, add // cCursor and hide the mouse. this.parent.addEventListener(MouseEvent.MOUSE_OVER, addMouseTrackF); // Remove cCursor and un-hide the mouse this.parent.addEventListener(MouseEvent.MOUSE_OUT, removeMouseTrackF); stage.addEventListener(KeyboardEvent.KEY_DOWN,keydownF); stage.addEventListener(KeyboardEvent.KEY_UP,keyupF); } private function shootF(e:MouseEvent):void{ // Create a shell var shell:Shell = new Shell(); // Add it to an array because I know I’ll // need to keep track of it and I think I // want to allow more than 1 shell to exist // at any one time. shellA.push(shell); // Add the shell to tank’s parent this.parent.addChild(shell); // Position the shell at the end of the // muzzle. (See Figure 6.1.) shell.x = this.turret_mc.x+this.x+gunL*Math.cos(this.turret_mc.rotation*Math.PI/180); shell.y = this.y+gunL*Math.sin(this.turret_mc.rotation*Math.PI/180); } private function addMouseTrackF(e:MouseEvent):void{ // Check the Mouse class for something to hide the mouse Mouse.hide();
Version _04 // Add cCursor to tank’s parent this.parent.addChild(cCursor); // Add a mousemove listener to update the cursor’s // x,y properties and the turret’s rotation this.parent.addEventListener(MouseEvent.MOUSE_MOVE, turnTurretF); } private function removeMouseTrackF(e:MouseEvent):void{ // Check the Mouse class for something to //un-hide the mouse Mouse.show() // Remove cCursor this.parent.removeChild(cCursor); // Remove the mousemove listener. We no longer // want the turret to rotate and there’s no // need to update cCursor’s x,y. this.parent.removeEventListener(MouseEvent.MOUSE_MOVE, turnTurretF); } private function turnTurretF(e:MouseEvent):void{ // Update cCursor’s x,y to be the same as the mouse’s // x,y relative to tank’s parent cCursor.x = e.localX; cCursor.y = e.localY; // Rotate the turret this.turret_mc.rotation = 180*Math.atan2(e.localY-this.y,e.localXthis.x-this.turret_mc.x)/Math.PI; } private function keydownF(e:KeyboardEvent):void{ } private function keyupF(e:KeyboardEvent):void{ } private function enterframeF(e:Event):void{ } private function moveLimitsF():void{ } } } 117
118 Chapter 6 n Developing a Flash Game shell.x = Math.cos (turret_mc.rotation*Math.PI/180) shell.y = Math.sin (turret_mc.rotation*Math.PI/180) turret_mc.rotation (in degrees). To convert degrees to radians, multiply by Math.PI/180. gunL ( = gun length) Figure 6.1 Use trigonometry to position shells at the muzzle’s end. Source: © 2013 Keith Gladstien, All Rights Reserved. And test. The custom cursor is doing some funky jumping around, as if it’s being repeatedly removed and added. We added a trace() to removeMouseTrackF() to see whether the mouseout event is being triggered. (I thought the mouseout might be triggered if we moused over the tank, but I didn’t expect it when moving over that parent rectangle.) After checking with that trace(), we can see our mouseout is being triggered every time we move the mouse unless we move the mouse slowly. The only thing that could be doing that is the custom cursor! We’re mousing over the custom cursor, and that is triggering a mouseout of the custom cursor’s parent. But, it doesn’t seem like the custom cursor should be lagging that far behind the mouse. Let’s remove the mouseout trace() and add: trace(getTimer(),cCursor.x,e.localX); to turnTurretF() to see what happens when we move the mouse rapidly.
Version _05 Whoa, you can see the custom cursor’s position is lagging far behind the mouse even though it updates every few milliseconds. I thought it might be updating infrequently because of the MouseEvent.MOUSE_MOVE event, but the trace() reveals that isn’t the issue. But, the mouse easily moves 30 pixels in less than 10 milliseconds with no effort, causing the mouse to mouseover the custom cursor and thereby mouseout of its parent rectangle. One solution is to use a rollout event. The rollout event will be triggered only if you roll out of the parent and all its children (which, so far, comprises the player’s tank and the custom cursor). And our shell placement looks good except when we rotate the tank. We forgot to adjust the muzzle position when the tank is rotated. We need to rework it. But just thinking about the muzzle position is giving me a headache because the rotation point for the tank and the rotation point of the turret are offset. We’ll need to do some pencil and paper calculations to figure out the correct code, and then Flash will be burdened with that unduly complex calculation. However, we can simplify that calculation (for both Flash and us) if we make the tank’s rotation point the same as the turret’s rotation point. That’s a better resolution than using the more complex calculation needed if the tank and turret have different rotation points. Version _05 has an updated FLA and unchanged from version _04. PlayerTank Version _05 Version _05 PlayerTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.ui.Mouse; public class PlayerTank extends MovieClip { private var speed:int = 3; private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; private var prevX:Number; private var prevY:Number; private var keyCodeIndex:int; class. The other classes remain 119
120 Chapter 6 n Developing a Flash Game private var directionA:Array = []; private var boundaryViolation:Boolean; private var cCursor:CustomCursor; private var shellA:Array = []; private var gunL:Number; public function PlayerTank(_xLimit:int,_yLimit:int) { xLimit = _xLimit; yLimit = _yLimit; this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ // initial values for prevX and prevY, though // these aren’t needed unless this instance // starts on a boundary. prevX = this.x; prevY = this.y; // Tank gun length from center of turret top // to muzzle gunL = this.turret_mc.width-this.turret_mc.top_mc.width/2; // Create a custom cursor cCursor = new CustomCursor(); // Add a mouse down listener (to the tank’s // parent, which is the rectangle within which // the tank moves) to shoot this.parent.addEventListener(MouseEvent.MOUSE_DOWN,shootF); // When the mouse is over the tank’s parent, // add cCursor and hide the mouse. this.parent.addEventListener(MouseEvent.ROLL_OVER, addCustomCursorF); // Remove cCursor and un-hide the mouse this.parent.addEventListener(MouseEvent.ROLL_OUT, removeCustomCursorF); stage.addEventListener(KeyboardEvent.KEY_DOWN,keydownF); stage.addEventListener(KeyboardEvent.KEY_UP,keyupF); } private function shootF(e:MouseEvent):void{ // Create a shell var shell:Shell = new Shell(); // Add it to an array because I know I’ll need to // keep track of it and I think I want to allow // more than 1 shell to exist at any one time. shellA.push(shell); // Add the shell to tank’s parent this.parent.addChild(shell);
Version _05 // Position the shell at the end of the // muzzle. (See Figure 6.2.) shell.x = this.x+gunL*Math.cos((this.turret_mc.rotation+this.rotation)*Math.PI/180); shell.y = this.y+gunL*Math.sin((this.turret_mc.rotation+this.rotation)*Math.PI/180); } private function addCustomCursorF(e:MouseEvent):void{ this.addEventListener(Event.ENTER_FRAME,rotateTurretF); // Check the Mouse class for something // to hide the mouse Mouse.hide(); // Add cCursor to tank’s parent this.parent.addChild(cCursor); // Add a mousemove listener to update the // cursor’s x,y properties and the turret’s rotation //this.parent.addEventListener(MouseEvent.MOUSE_MOVE, // rotateTurretF); } private function removeCustomCursorF(e:MouseEvent):void{ this.removeEventListener(Event.ENTER_FRAME,rotateTurretF); // Check the Mouse class for something to un-hide // the mouse Mouse.show() // Remove cCursor this.parent.removeChild(cCursor); // Remove the mousemove listener. We no longer want // the turret to rotate and there’s no need to // update cCursor’s x,y. //this.parent.removeEventListener(MouseEvent.MOUSE_MOVE, // rotateTurretF); } private function rotateTurretF(e:Event):void{ // Update cCursor’s x,y to be the same as the //mouse x,y relative to tank’s parent cCursor.x = this.parent.mouseX; cCursor.y = this.parent.mouseY; // Rotate the turret this.turret_mc.rotation = -this.rotation+180*Math.atan2(this.parent.mouseY-this.y,this.parent.mouseX-this.xthis.turret_mc.x)/Math.PI; } // I revised KBcontrols so it stores the 4 tank // control keycodes in a static array keyCodeA 121
122 Chapter 6 n Developing a Flash Game // The first element (with index 0) is the forward // keycode; the 2nd, 3rd, and 4th elements are back, // left, and right, resp. // using a keyCodeA allows me to avoid using for // loops to detect if a pressed key is a control key. private function keydownF(e:KeyboardEvent):void{ // Here is precisely how using keyCodeA // eliminates the need for a for loop: // arrays have an indexOf property that // allows a quick way to check if a value // is an array element. In addition, it // returns the index of the found value. // Because the 0-index is the forward key // and 1-index is the back key, I can // efficiently determine not only if a // control key was pressed, but exactly // which control key. keyCodeIndex = KBcontrols.keyCodeA.indexOf(e.keyCode); // if keyCodeIndex==-1, e.keyCode is not // in keyCodeA. Otherwise, it is and its value // is the index of e.keyCode in keyCodeA if(keyCodeIndex>-1){ // by checking which directionA elements // are true, I can determine which control // keys are currently pressed. directionA[keyCodeIndex] = true; } this.addEventListener(Event.ENTER_FRAME,moveTankF); } private function keyupF(e:KeyboardEvent):void{ // Same code as above. Only in keyupF, I’m going // to set the corresponding value in directionA // to false to indicate this key is not currently // being pressed. keyCodeIndex = KBcontrols.keyCodeA.indexOf(e.keyCode); if(keyCodeIndex>-1){ directionA[keyCodeIndex] = false; } if(directionA.indexOf(true)==-1){ this.removeEventListener(Event.ENTER_FRAME,moveTankF); } } private function moveTankF(e:Event):void{ // No if-else statements here now. I want to
Version _05 // change rotation and position if two appropriate // control keys are pressed together. if(directionA[0]){ //trace("foreward"); this.x += speed*Math.cos(this.rotation*Math.PI/180); this.y += speed*Math.sin(this.rotation*Math.PI/180); } if(directionA[1]){ //trace("back"); this.x -= speed*Math.cos(this.rotation*Math.PI/180); this.y -= speed*Math.sin(this.rotation*Math.PI/180); } if(directionA[2]){ //trace("left"); this.rotation -= rotationRate; } if(directionA[3]){ //trace("right"); this.rotation += rotationRate; } // Check for boundary violation. Notice prevX,prevY // are updated after moveLimitsF() executes. moveLimitsF(); prevX = this.x; prevY = this.y; } private function moveLimitsF():void{ // Check if any of the 4 boundaries are violated. // If one is, no need to check others, so if-else // appropriate here. boundaryViolation = false; if(this.x<this.width/2){ boundaryViolation = true; } else if(this.x>xLimit-this.width/2){ boundaryViolation = true; } else if(this.y<this.height/2){ boundaryViolation = true; } else if(this.y>yLimit-this.height/2){ boundaryViolation = true; } // If there is a boundary violation, reset both x,y // to the pre-violation values. if(boundaryViolation){ this.x = prevX; 123
124 Chapter 6 n Developing a Flash Game this.y = prevY; } } } } shell.x = Math.cos ((turret_mc.rotation+ this.rotation)*Math.PI/180) shell.y = Math.sin ((turret_mc.rotation+this. rotation)*Math.PI/180) tank rotation (this.rotation in the scope of PlayerTank) turret_mc.rotation (in degrees). To convert to radians multiply by Math.PI/180. gunL ( = gun length) Figure 6.2 Slightly more trigonometry to account for the tank’s rotation. Source: © 2013 Keith Gladstien, All Rights Reserved. Testing version_05 reveals that everything is working. During testing we added trace() statements to rotateTurretF() and moveTankF() to make sure each loop terminates when the mouse leaves the tank parent MovieClip and when no keys are pressed, respectively. But I have a confession. This is not my version _05. I actually made a different version that used an enterframeF that called rotateTurretF and moveTankF and one enterFrame loop. It has always (or for at least as long as I can remember) been my feeling that one enterFrame loop that called two or more other functions was more efficient than two or more enterFrame loops each calling one function.
Version _05 As I was starting to explain (for this book) why I made one enterFrame loop to call rotateTurretF and moveTankF, I thought I should run a test to confirm that feeling. When I did, I found that one enterFrame loop calling many functions was slower than many enterFrame loops each calling one function. (The test code is in enterframe_test_one_v_many_loops_with_one_mc, if you want to check yourself.) But, they are close to the same speedwise for the number of functions realistically encountered. Also, using different loops is often easier to code, and fewer variables are needed because when you’re using one main loop, that loop needs to use Booleans to determine whether to call the other functions and to determine when to remove that main enterFrame loop. Note, this tests one versus many enterFrame loops applied to one MovieClip. It doesn’t test whether it is more efficient for each MovieClip (in this context, tank) to have its own loops (as currently set up) or whether it is more efficient to have one master MovieClip with its own loops controlling all the tanks. In the next chapter, I will address that. It’s time for the next version. We want to add two more features to PlayerTank (a shot sound and a moving shell) and also a combat view with a PlayerTank and an EnemyTank instance. To start that, we need to edit Main so it removes introView and adds a combat view. Before Main can add a combat view, we’ll make that barebones MovieClip symbol in Flash. We’ll add an arena_mc (similar to playerBG_mc in the intro view library symbol) that will be used to establish tank (and shell boundaries). We want to leave room for a timer and a stats display. We’ll assign it the CombatView class, which we’ll start coding by copying the IntroView code, editing out the unneeded listeners, and adding the code needed for a PlayerTank instance and an EnemyTank instance. Then we need an EnemyTank class, which we’ll start coding by copying the PlayerTank code and editing out everything unnecessary, such as the keyboard event listeners. We’ll add some code to color these tanks so they look different from the PlayerTank instances. At this point, we should use the _tank mc symbol for both PlayerTank and EnemyTank. So, we’ll change the _tank mc class (double-click in the AS Linkage column next to _tank mc) and assign it the com.kglad.Tank class. 125
126 Chapter 6 n Developing a Flash Game Now we can use that symbol for PlayerTank and EnemyTank by having PlayerTank and EnemyTank extend the Tank class. And, we’re set up to actually create that class code and eliminate the duplicate code in PlayerTank and EnemyTank. I have updated the FLA to tank_combat_06.fla; changed _06 versions of IntroView, PlayerTank; and created new CombatView, EnemyTank, and Tank classes. Main, Version _06 Version _06 Main.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; public class Main extends MovieClip { // I removed the currentView variable because I // have different listeners (that need to be removed) // for the different views. So, I decided it was // easier to follow and maintain this code by // explicitly tracking each view. private var introView:IntroView; private var combatView:CombatView; public function Main() { addIntroViewF(); } private function addIntroViewF():void{ introView = new IntroView(); // I changed startgameE and startgameF to // startCombatE and addCombatViewF, resp, // to make it easier for me to see what they // do. They are just names so I can use almost // anything I want. Picking good names can make // understanding your code much easier not only // for others (which, except for writing this // book, has never been a factor for me), but // also for yourself, and that has always been // an issue for me. It is not unusual for me to // edit something I have not looked at for months // or even years. introView.addEventListener("startCombatE",addCombatViewF); addChild(introView); } private function addCombatViewF(e:Event):void{ // Ready introView for gc
Version _06 introView.removeEventListener("startCombatE",addCombatViewF); removeChild(introView); introView = null; // Add a CombatView instance combatView = new CombatView(); combatView.addEventListener("gameOverE",addGameOverViewF); addChild(combatView); } private function addGameOverViewF(e:Event):void{ combatView.removeEventListener("gameOverE",addGameOverViewF); removeChild(combatView); combatView = null; // To be done trace("gameover", "Main"); } } } The only thing changed in IntroView is some renaming of and changing the dispatchEvent event name: startgame to dispatchEvent(new Event("startCombatE")); Version _06 CombatView.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; public class CombatView extends MovieClip { private var player:PlayerTank; // In case I want to add more than 1 enemy tank private var enemyNum:int = 1; // If you use, var enemyA:Array here and fail to // create an array instance (using brackets or // the new Array() constructor), you will // trigger a 1009 error when you try to apply // any array property or method to enemyA private var enemyA:Array = []; public function CombatView() { this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ // Define all the click listeners. listenersF(); // Stub in the code for a stats display statsDisplayF(); startCombat 127
128 Chapter 6 n Developing a Flash Game // Add player tank and enemy tank(s) addTanksF(); } private function listenersF():void{ // To be done } private function statsDisplayF():void{ // To be done } private function addTanksF():void{ var w:int = arena_mc.width; var h:int = arena_mc.height // create a player instance passing w and h // used to restrict player’s movement // (in PlayerTank) player = new PlayerTank(w,h); // Position player using arena_mc as the tank parent positionF(arena_mc,player); // Add enemy tank(s) for(var i:int=0;i<enemyNum;i++){ var enemy:EnemyTank = new EnemyTank(w,h); // Position enemy tank(s) positionF(arena_mc,enemy); enemyA.push(enemy); } } private function positionF(parent_mc:MovieClip,mc:MovieClip):void { parent_mc.addChild(mc); // I want a little distance between tanks and // the arena edges mc.x = int(mc.width+(parent_mc.width-2*mc.width)*Math.random()); mc.y = int(mc.height+(parent_mc.height-2*mc.height)*Math.random()); // If this is an enemy tank make sure it’s not // hitting another tank. player is added first so // no need to check for a hit when it’s added if(mc!=player){ // check if mc is hitting player if(player.hitTestObject(mc)){ // If there’s a positive hit test, call // positionF() again and (almost // certainly) try another position positionF(parent_mc,mc); } else { // check mc doesn’t hit any enemyA
Version _06 // element. Notice mc is not in // enemyA, yet. for(var i:int=0;i<enemyA.length;i++){ if(mc.hitTestObject(enemyA[i])){ positionF(parent_mc,mc); // There is no need to check for // another hit so terminate this // for loop. break; } } } } } } } Version _06 Tank.as package com.kglad { import flash.display.MovieClip; import flash.media.Sound; import flash.events.Event; import flash.events.MouseEvent; public class Tank extends MovieClip{ private var xLimit:int; private var yLimit:int; // I need to liberalize from private or will get // an 1178 error: Attempted access of inaccessible // property prevX through a reference with static // type com.kglad:PlayerTank. internal var prevX:Number; internal var prevY:Number; private var boundaryViolation:Boolean; private var shellA:Array = []; private var gunL:Number; private var shellSpeed:int = 10; private var shotSound:Sound; public function Tank(_xLimit:int,_yLimit:int) { xLimit = _xLimit; yLimit = _yLimit; this.addEventListener(Event.ADDED_TO_STAGE,initT); } private function initT(e:Event):void{ prevX = this.x; 129
130 Chapter 6 n Developing a Flash Game prevY = this.y; gunL = this.turret_mc.width-this.turret_mc.top_mc.width/2; } // I want to call shootF from my PlayerTank and // EnemyTank subclasses so I can use the protected // or any attribute other than private. protected function shootF(e:MouseEvent):void{ // create a shot sound. I could use // var shotSound:ShotSound = new ShotSound() // here but then I would be creating many // shotSound variables. They are local to // shootF() so they would be readily gc’d but it // is more efficient to create one variable and // reuse it. Actually, it would be even more // efficient to create one variable and one // ShotSound(), so in the next version // I’ll clean that up. shotSound = new ShotSound(); shotSound.play(); var shell:Shell = new Shell(); shellA.push(shell); this.parent.addChild(shell); // shells are no longer stationary. I will need // to know where each shell should be after I // shoot it. I will use these parameters, // cos,sin,tankX,tankY and f. // I only need each shell’s angle, but rather // than repeatedly calculate the cosine and // sine of that angle, I can just save // those values as parameters and save some // cpu cycles. shell.cos = Math.cos((this.turret_mc.rotation+this.rotation)*Math.PI/180); shell.sin = Math.sin((this.turret_mc.rotation+this.rotation)*Math.PI/180); // shell’s initial position shell.tankX = this.x; shell.tankY = this.y; // I am going to increment f (the distance traveled // by the shell) by shellSpeed in each loop to update // the distance traveled by the shell. Initially the // shell’s distance from this.x,this.y is gunL shell.f = gunL; // I may as well use the parameters here to ease my coding.
Version _06 shell.x = this.x+gunL*shell.cos; shell.y = this.y+gunL*shell.sin; // Add an enterframe loop to update where each shell // should be. No reason to keep adding it if it’s // already been added. if(shellA.length==1){ // And I should remove this listener when // shellA.length is zero. I’ll fix that // in the next version. this.addEventListener(Event.ENTER_FRAME,shellLoopF); } } private function shellLoopF(e:Event):void{ // Always loop through arrays from end to start // IF you might be removing array elements. Otherwise, // you skip an array element every time an element // is removed. for(var i:int=shellA.length-1;i>=0;i– –){ // Update the distance the shell has traveled shellA[i].f+=shellSpeed; // Assign its x,y using the parameters // defined in shootF() shellA[i].x = shellA[i].tankX+shellA[i].f*shellA[i].cos; shellA[i].y = shellA[i].tankY+shellA[i].f*shellA[i].sin; // Check if the shell should be removed shellRemoveCheckF(shellA[i],i); } } private function shellRemoveCheckF(shell:Shell,i:int):void{ // I created a boundaryViolationF() function to // use for the tank and shells so I don’t have // to rewrite the same code. if(boundaryViolationF(shell)){ // ready shellA[i] for gc this.parent.removeChild(shellA[i]); // splice(i,1) is an array method that // removes the ith element when the second // parameter is one. And here is where you // would encounter a problem if you were // looping from the start to the end of an // array. After removing the ith element, // what was the i+1 element is now the ith // element. When your for-loop counter 131
132 Chapter 6 n Developing a Flash Game // increments, it skips the old i+1 element // that is now the ith element. // That might only be inefficient, but it // can be disastrous. shellA.splice(i,1); } } protected function moveLimitsF():void{ if(boundaryViolationF(this)){ this.x = prevX; this.y = prevY; } } Private function boundaryViolationF(mc:MovieClip):Boolean{ boundaryViolation = false; if(mc.x<mc.width/2){ boundaryViolation = true; } else if(mc.x>xLimit-mc.width/2){ boundaryViolation = true; } else if(mc.y<mc.height/2){ boundaryViolation = true; } else if(mc.y>yLimit-mc.height/2){ boundaryViolation = true; } return boundaryViolation; } } } Version _06 PlayerTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.ui.Mouse; public class PlayerTank extends Tank { private var speed:int = 3; private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; // These are already declared in the Tank class. As long // as I use an attribute (ie, anything but protected) in // the Tank class that allows these and other variables
Version _06 // to be accessible to subclasses, I do not need to declare // them here. //private var prevX:Number; //private var prevY:Number; private var keyCodeIndex:int; private var directionA:Array = []; public function PlayerTank(_xLimit:int,_yLimit:int) { // You need to call the superclass constructor (Tank) // or you’ll trigger a 1203 error: No default // constructor found in base class // com.kglad:Tank error. super(_xLimit,_yLimit); cCursor = new CustomCursor(); this.addEventListener(Event.ADDED_TO_STAGE,initP); } private function initP(e:Event):void{ this.parent.addEventListener(MouseEvent.MOUSE_DOWN,shootF); this.parent.addEventListener(MouseEvent.ROLL_OVER, addCustomCursorF); this.parent.addEventListener(MouseEvent.ROLL_OUT, removeCustomCursorF); stage.addEventListener(KeyboardEvent.KEY_DOWN,keydownF); stage.addEventListener(KeyboardEvent.KEY_UP,keyupF); } private function addCustomCursorF(e:MouseEvent):void{ // no change } private function removeCustomCursorF(e:MouseEvent):void{ // no change } private function rotateTurretF(e:Event):void{ // no change } private function keydownF(e:KeyboardEvent):void{ // no change } private function keyupF(e:KeyboardEvent):void{ // no change } private function moveTankF(e:Event):void{ // no change } } } 133
134 Chapter 6 n Developing a Flash Game Version _06 EnemyTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.ui.Mouse; import flash.geom.ColorTransform; import flash.media.Sound; public class EnemyTank extends Tank { private var speed:int = 3; private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; // Everything else declared in Tank Class public function EnemyTank(_xLimit:int,_yLimit:int) { // Call the Tank constructor super(_xLimit,_yLimit); // color this enemy colorF(); } private function colorF():void{ // Use the color propery of the ColorTransform // class to assign colors to display objects. var ct:ColorTransform = new ColorTransform(); ct.color = 0x000099; this.base_mc.transform.colorTransform = ct; ct.color = 0x000066; this.turret_mc.top_mc.transform.colorTransform = ct; } private function rotateTurretF(e:Event):void{ // To be done: Rotate the turret } private function moveTankF(e:Event):void{ // To be done: Update tank position, rotation. moveLimitsF(); //prevX = this.x; //prevY = this.y; } } }
Version _07 After testing, we can see this works pretty well, except that we have to click on the combat view stage to start detecting key presses. We added a trace() to PlayerTank’s keydownF and found two traces for each key press. That can occur only if we have two keydown listeners. I was going to address readying objects for gc later, but I cannot put that off any longer. We have been sloppy about creating objects and not readying them for gc (garbage collection) when they’re no longer needed. This is a good time to do that because we’re already seeing a problem: We have created, among other problematic things, two different stage keydown listeners, and they are both calling our combat view PlayerTank instance’s keydownF. That is, each keypress results in two calls to keydownF. Further, our sloppiness caused the combat view problem that required the stage to be clicked for those listeners to work. Using trace(stage.focus) in Main’s AddCombatViewF(), you found that when you clicked the startCombat_mc, focus was changed to startCombat_mc. But then, startCombat_mc’s parent was removed from the display list (which removed startCombat_mc from the display list), so nothing related to the stage had focus. We could remedy the stage focus problem by assigning the stage.focus property, but it’s better to clean up everything in case more problems are lurking. In addition to those changes in the next version, we added movement and shooting code to EnemyTank. There were no changes to the FLA, Main, or KBcontrols. Version _07 Version _07 IntroView.as package com.kglad { import flash.display.MovieClip; import flash.events.MouseEvent; import flash.events.KeyboardEvent; import flash.events.Event; public class IntroView extends MovieClip { private var player:PlayerTank; public function IntroView() { this.addEventListener(Event.ADDED_TO_STAGE,init); // Clean up everything in this class when // no longer needed. That is, when // removeChild(introView) executes in Main, // the Event.REMOVED_FROM_STAGE event will // be dispatched and removeF() will be called. 135
136 Chapter 6 n Developing a Flash Game this.addEventListener(Event.REMOVED_FROM_STAGE,removeF); } private function init(e:Event):void{ // I may as well remove this listener now. It // is not needed after detecting the // Event.ADDED_TO_STAGE event once. this.removeEventListener(Event.ADDED_TO_STAGE,init); listenersF(); addPlayerTankF(); } private function removeF(e:Event):void{ this.removeEventListener(Event.REMOVED_FROM_STAGE,removeF); // Remove all the listeners created in IntroView removeListenersF(); // Remove everything including that problematic // startCombat_mc button that had focus and was // then removed from the stage. removeAllF(); } private function listenersF():void{ left_mc.addEventListener(MouseEvent.CLICK,leftF); left_mc.buttonMode = true; right_mc.addEventListener(MouseEvent.CLICK,rightF); right_mc.buttonMode = true; forward_mc.addEventListener(MouseEvent.CLICK,forwardF); forward_mc.buttonMode = true; back_mc.addEventListener(MouseEvent.CLICK,backF); back_mc.buttonMode = true; startCombat_mc.addEventListener(MouseEvent.CLICK,startCombatF); startCombat_mc.buttonMode = true; } private function removeListenersF():void{ left_mc.removeEventListener(MouseEvent.CLICK,leftF); right_mc.removeEventListener(MouseEvent.CLICK,rightF); forward_mc.removeEventListener(MouseEvent.CLICK,forwardF); back_mc.removeEventListener(MouseEvent.CLICK,backF); startCombat_mc.removeEventListener(MouseEvent.CLICK,startCombatF); } private function removeAllF():void{ // This is one way to remove all children of a // DisplayObjectContainer. Another is to loop // through the depths backward, similar to // removing elements from an array. for(var i:int=0;i<this.numChildren;i++){
Version _07 removeChildAt(0); } } // The rest of this class is unchanged. } } Version _07 CombatView.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; public class CombatView extends MovieClip { private var player:PlayerTank; // In case I want to add more than 1 enemy tank private var enemyNum:int = 1; // If you use, var enemyA:Array here and fail to // create an array instance, var enemyA:Array = [] // or var enemyA:Array = new Array(), // you will get a 1009 error when you try to apply // any array property or method to enemyA private var enemyA:Array = []; var hitBool:Boolean; public function CombatView() { this.addEventListener(Event.ADDED_TO_STAGE,init); // Again, the Event.REMOVED_FROM_STAGE // event listener to use for clean up this.addEventListener(Event.REMOVED_FROM_STAGE,removedF); } private function init(e:Event):void{ // Again, remove the Event.ADDED_TO_STAGE // listener as soon as it has completed // its only job. this.removeEventListener(Event.ADDED_TO_STAGE,init); // define all the click listeners listenersF(); statsDisplayF(); // Add player tank and enemy tank(s) addTanksF(); } private function listenersF():void{ // To be done } private function statsDisplayF():void{ // To be done 137
138 Chapter 6 n Developing a Flash Game } private function addTanksF():void{ var w:int = arena_mc.width; var h:int = arena_mc.height player = new PlayerTank(w,h); positionF(arena_mc,player); // Add enemy tank(s) for(var i:int=0;i<enemyNum;i++){ // Pass a reference to this class // instance so enemy can access // player. See playerT() getter below. var enemy:EnemyTank = new EnemyTank(this,w,h); // Position enemy tank(s) positionF(arena_mc,enemy); enemyA.push(enemy); } } private function positionF(arena_mc:MovieClip,mc:MovieClip):void { // no change } // Allow access to player in EnemyTank to determine // player’s position and, in a later version, // possible shell trajectories for evasive // manuevers. public function get playerT():PlayerTank{ return player; } // Remove the remaining listener and added displayobjects private function removedF(e:Event):void{ this.removeEventListener(Event.REMOVED_FROM_STAGE,removedF); for(var i:int=0;i<arena_mc.numChildren;i++){ arena_mc.removeChildAt(0); } } } } Version _07 Tank.as package com.kglad { import flash.display.MovieClip; import flash.media.Sound; import flash.events.Event; import flash.events.MouseEvent; public class Tank extends MovieClip{
Version _07 private var xLimit:int; private var yLimit:int; protected var prevX:Number; protected var prevY:Number; private var shellA:Array = []; private var gunL:Number; private var shellSpeed:int = 10; // Here’s the more efficient code for shotSound. // I can apply the play() method to shotSound // repeatedly for each shot private var shotSound:Sound = new ShotSound(); // I decided to limit the maximum number of // shells that can exist on-screen at any one // time so this is not a blast-fest. private var maxShots:int = 1; public function Tank(_xLimit:int,_yLimit:int) { xLimit = _xLimit; yLimit = _yLimit; this.addEventListener(Event.ADDED_TO_STAGE,initT); // Clean up this.addEventListener(Event.REMOVED_FROM_STAGE, removedF); } private function initT(e:Event):void{ this.removeEventListener(Event.ADDED_TO_STAGE,initT); prevX = this.x; prevY = this.y; gunL = this.turret_mc.width-this.turret_mc.top_mc.width/2; } protected function shootF(e:MouseEvent):void{ // Here’s where I limit the number of shells // that can exist at any one time if(shellA.length<maxShots){ // Here’s the more efficient code at work // using the same one variable referencing // the same sound instance shotSound.play(); var shell:Shell = new Shell(); shellA.push(shell); this.parent.addChild(shell); shell.cos = Math.cos((this.turret_mc.rotation+this.rotation)*Math.PI/180); shell.sin = Math.sin((this.turret_mc.rotation+this.rotation)*Math.PI/180); shell.tankX = this.x; 139
140 Chapter 6 n Developing a Flash Game shell.tankY = this.y; shell.f = gunL; shell.x = this.x+gunL*shell.cos; shell.y = this.y+gunL*shell.sin; if(shellA.length==1){ this.addEventListener(Event.ENTER_FRAME,shellLoopF); } } } private function shellLoopF(e:Event):void{ for(var i:int=shellA.length-1;i>=0;i– –){ shellA[i].f+=shellSpeed; shellA[i].x = shellA[i].tankX+shellA[i].f*shellA[i].cos; shellA[i].y = shellA[i].tankY+shellA[i].f*shellA[i].sin; shellRemoveCheckF(shellA[i],i); } } private function shellRemoveCheckF(shell:Shell,i:int):void{ if(boundaryViolationF(shell)){ this.parent.removeChild(shellA[i]); shellA.splice(i,1); } // Remove enterframe loop if no more shells if(shellA.length==0){ this.removeEventListener(Event.ENTER_FRAME,shellLoopF); } } protected function moveLimitsF():void{ if(boundaryViolationF(this)){ this.x = prevX; this.y = prevY; } } protected function boundaryViolationF(mc:MovieClip):String{ // I made some changes here so I could // determine which boundary was violated. // I thought that might be needed so EnemyTank // instances could change direction // intelligently when a boundary was violated. // But, so far, that hasn’t been needed. if(mc.x<mc.width/2){ return "L"; } else if(mc.x>xLimit-mc.width/2){
Version _07 return "R"; } else if(mc.y<mc.height/2){ return "T"; } else if(mc.y>yLimit-mc.height/2){ return "B"; } return ""; } // Only shell instances might need to be removed. private function removedF(e:Event):void{ this.removeEventListener(Event.REMOVED_FROM_STAGE,removedF); if(shellA.length>0){ // if shellA.length==0, this loop has // already been removed this.removeEventListener(Event.ENTER_FRAME,shellLoopF); for(var i:int=shellA.length-1;i>=0;i– –){ if(shellA[i].parent){ shellA[i].parent.removeChildAt(0); } } } } } } Version _07 PlayerTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.ui.Mouse; public class PlayerTank extends Tank { private var speed:int = 3; private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; private var keyCodeIndex:int; private var directionA:Array = []; private var cCursor:CustomCursor; public function PlayerTank(_xLimit:int,_yLimit:int) { super(_xLimit,_yLimit); cCursor = new CustomCursor(); this.addEventListener(Event.ADDED_TO_STAGE,initP); 141
142 Chapter 6 n Developing a Flash Game // The usual listener for clean-up purposes. this.addEventListener(Event.REMOVED_FROM_STAGE,removedP); } private function initP(e:Event):void{ this.removeEventListener(Event.ADDED_TO_STAGE,initP); this.parent.addEventListener(MouseEvent.MOUSE_DOWN,shootF); this.parent.addEventListener(MouseEvent.ROLL_OVER, addCustomCursorF); this.parent.addEventListener(MouseEvent.ROLL_OUT, removeCustomCursorF); stage.addEventListener(KeyboardEvent.KEY_DOWN,keydownF); stage.addEventListener(KeyboardEvent.KEY_UP,keyupF); } private function addCustomCursorF(e:MouseEvent):void{ // no change } private function removeCustomCursorF(e:MouseEvent):void{ // no change } private function rotateTurretF(e:Event):void{ // no change } private function keydownF(e:KeyboardEvent):void{ // no change } private function keyupF(e:KeyboardEvent):void{ // no change } private function moveTankF(e:Event):void{ // no change except no need to call // rotateTurret(e) from here // rotateTurretF(e); } private function removedP(e:Event):void{ this.removeEventListener(Event.REMOVED_FROM_STAGE,removedP); this.parent.removeEventListener(MouseEvent.MOUSE_DOWN,shootF); this.parent.removeEventListener(MouseEvent.ROLL_OVER, addCustomCursorF); this.parent.removeEventListener(MouseEvent.ROLL_OUT, removeCustomCursorF); stage.removeEventListener(KeyboardEvent.KEY_DOWN,keydownF); stage.removeEventListener(KeyboardEvent.KEY_UP,keyupF); this.removeEventListener(Event.ENTER_FRAME,moveTankF);
Version _07 this.removeEventListener(Event.ENTER_FRAME,rotateTurretF); } } } Version _07 EnemyTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.geom.ColorTransform; import flash.utils.Timer; import flash.utils.getTimer; import flash.events.TimerEvent; public class EnemyTank extends Tank { private var speed:int = 3; private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; // Used to access player so enemy tank knows where to shoot private var cv:CombatView; // Current direction in degrees (0 to 359). When direction // changes, update distance to speed and startX,startY to // current x,y private var currentDirection:int; // Next direction in degrees. Must gradually change // currentDirection to nextDirection. private var nextDirection:int; // These next three variables are used in tank movement. private var distance:int; private var startX:int; private var startY:int; // Frequency of random direction changes (evasive // manuevering) in ms private var directionChangeFreq:int = 2500; // This is a degrees to radians constant because I finally // got tired of typing the same thing repeatedly. I should // actually define this in Tank and use it in Tank, PlayerTank // and EnemyTank. That will be in the next update. private const d2r:Number = Math.PI/180; // Timer for direction changes private var directionChangeTimer:Timer; // Boolean used to indicate whether this tank is moving // backwards. (Used when there is a boundary violation.) 143
144 Chapter 6 n Developing a Flash Game private var moveBackBool:Boolean; // Timer used to stop moving backwards from a boundary. // That is, when a boundary is encountered, an EnemyTank // instance will move backwards without changing direction // until moveBackTimer dispatches a TimerEvent.TIMER event. private var moveBackTimer:Timer; public function EnemyTank(_cv:CombatView,_xLimit:int,_yLimit:int) { super(_xLimit,_yLimit); colorF(); cv = _cv; // Initialize currentDirection currentDirection = int(360*Math.random()); // Initialize distance distance = speed; this.addEventListener(Event.ADDED_TO_STAGE,init); this.addEventListener(Event.REMOVED_FROM_STAGE,removedF); } private function init(e:Event):void{ this.removeEventListener(Event.ADDED_TO_STAGE,init); // Start loops for tank movement and // turret rotation this.addEventListener(Event.ENTER_FRAME,moveTankF); this.addEventListener(Event.ENTER_FRAME,rotateTurretF); // Timer used to change directions (and // make this tank’s position more difficult // to predict and thereby more difficult to // shoot). directionChangeTimer = new Timer(directionChangeFreq,0); directionChangeTimer.addEventListener(TimerEvent.TIMER,newDirectionF); directionChangeTimer.start(); // Timer to terminate moving backward after // boundary violation moveBackTimer = new Timer(1000,1); moveBackTimer.addEventListener(TimerEvent.TIMER,moveBackF); } private function newDirectionF(e:TimerEvent):void{ // ai: turn towards stage center. Determine // which quadrant the tank is currently located // and then assign nextDirection to be towards // stage-center. That should help prevent this // tank from getting trapped in a corner. var quadrant:String; if(this.x<this.parent.width/2){ quadrant="L";
Version _07 } else { quadrant="R"; } if(this.y<this.parent.height/2){ quadrant+="U"; } else { quadrant+="D"; } if(quadrant=="LU"){ nextDirection = int(90*Math.random()); } else if(quadrant=="LD"){ nextDirection = 270+int(90*Math.random()); } else if(quadrant=="RU"){ nextDirection = 90+int(90*Math.random()); } else { nextDirection = 180+int(90*Math.random()); } // start loop to change from currentDirection // to nextDirection this.addEventListener(Event.ENTER_FRAME,directionChangeF); } private function colorF():void{ var ct:ColorTransform = new ColorTransform(); ct.color = 0x0000cc; this.base_mc.transform.colorTransform = ct; ct.color = 0x000066; this.turret_mc.top_mc.transform.colorTransform = ct; } private function rotateTurretF(e:Event):void{ // Aim towards player (retrieved via CombatView // instance cv’s playerT getter) shootF(null); // I want this.turret_mc.rotataion+this.rotation // to be equal to the angle from this to cv.playerT. // The angle from this EnemyTank instance to // cv.playerT is easily determined just like // previously discussed in chapter 4 using // Math.atan2(). this.turret_mc.rotation = -this.rotation+180*Math.atan2(cv.playerT.y-this.y,cv.playerT.x-this.x)/Math.PI; } private function moveTankF(e:Event):void{ // Initialize startX,startY. Note this cannot 145
146 Chapter 6 n Developing a Flash Game // be done in init() because init() is called // when this tank is added to the display list // and before its x,y are reset from 0,0 in // positionF(), whereas the first enterframe loop // always occurs some milliseconds after it’s // defined (and after this tank is positioned). if(startX==0){ startX = this.x; startY = this.y; } // If moving backwards don’t rotate towards // direction of movement. Otherwise, rotate // to movement direction. if(!moveBackBool){ this.rotation = currentDirection; } // Update position based on the variables // startX,startY,distance and currentDirection. this.x = startX+distance*Math.cos(currentDirection*d2r); this.y = startY+distance*Math.sin(currentDirection*d2r); // Update distance distance += speed; moveLimitsF(); prevX = this.x; prevY = this.y; } // Enemy tanks need more control over boundary // behavior than I could conveniently achieve in // Tank, so I’m overriding Tank’s moveLimitsF() // with the moveLimitsF() here. I need to use the // override attribute to inform Flash that this // moveLimitsF() will execute and not the one in // Tank. override protected function moveLimitsF():void{ if(boundaryViolationF(this)){ // Move to position prior to // boundary violation. this.x = prevX; this.y = prevY; // Get ready to move in opposite // direction. // Stop the evasive manuevering. // Must get off the boundary asap or this tank is doomed. directionChangeTimer.stop();
Version _07 // Reset distance, startX and startY directionChangeUpdateF(); // Change currentDirection by 180 // degrees. I modulo all my angles // by 360 to make it easy for me to // deal with these directions. currentDirection = (180+currentDirection)%360; // nextDirection will be (minus) 90 // degrees to the direction that led // to the boundary violation. nextDirection = (90+currentDirection)%360; // Enable moveBackBool so this tank looks // like it’s moving backwards and not // turning away from the boundary (yet). moveBackBool = true; // Start the moveBackTimer to stop this // tank moving backward and start it // moving forward and turning towards // nextDirection. moveBackTimer.start(); // Stop any direction change activity. // This tank won’t be ready to change // direction until we complete backing // away from boundary. this.removeEventListener(Event.ENTER_FRAME,directionChangeF); } } // This function is called when this tank is done // moving backwards from a boundary. private function moveBackF(e:TimerEvent):void{ // Reset those parameters. directionChangeUpdateF(); // No longer will this tank move backwards // so we can face the direction of movement moveBackBool = false; // Rechange currentDirection back towards the // boundary (but nextDirection is set to turn // -90 degrees so hopefully we won’t // hit the boundary again). currentDirection = (180+currentDirection)%360; // Restart evasive manuever intervals. directionChangeTimer.start(); // Start the loop to gradually change direction 147
148 Chapter 6 n Developing a Flash Game // from currentDirection to nextDirection so this // tank smoothly turns away from the boundary. this.addEventListener(Event.ENTER_FRAME,directionChangeF); } // Here’s where direction changes are made from // currentDirection to nextDirection. private function directionChangeF(e:Event):void{ // transition from currentDirection to nextDirection // the "short" way (explained below). if(currentDirection<=nextDirection){ if(nextDirection-currentDirection<=180){ // increment currentDirection currentDirection+=rotationRate; } else { // decrement currentDirection. (eg, // currentDirection=5 & nextDirection=355, // go the "short" way, 5 to 355 currentDirection = (currentDirectionrotationRate+360)%360; } } else { // currentDirection>nextDirection if(currentDirection-nextDirection<180){ // decrement currentDirection currentDirection-=rotationRate; } else { // increment currentDirection currentDirection = (currentDirection+rotationRate)%360; } } // If currentDirection and nextDirection are close, // stop the direction change loop if(Math.abs(currentDirection-nextDirection)<=rotationRate/2){ this.removeEventListener(Event.ENTER_FRAME,directionChangeF); } // Reset parameters with each direction change. // The same parameters are valid and don’t need // to be changed (with distance being incremented // by speed) while moving in a straight line. directionChangeUpdateF() }
Version _08 private function directionChangeUpdateF():void{ distance = speed; startX = this.x; startY = this.y; } private function removedF(e:Event):void{ // Clean up this.removeEventListener(Event.REMOVED_FROM_STAGE,removedF); this.removeEventListener(Event.ENTER_FRAME,moveTankF); this.removeEventListener(Event.ENTER_FRAME,rotateTurretF); this.removeEventListener(Event.ENTER_FRAME,directionChangeF); directionChangeTimer.removeEventListener(TimerEvent.TIMER,newDirectionF); moveBackTimer.removeEventListener(TimerEvent.TIMER,moveBackF); } } } And, test. This works pretty well. There’s a problem with the enemy tank shots not being perfect, likely caused by shooting and then updating the aim point instead of updating the aim point and then shooting. That is easily fixed. And, while I’m still not happy with the boundary behavior, it is now rare for an enemy tank to violate a boundary because of the turn toward stage-center coding. In fact, I like results of the turn toward stage-center so much, I think we should institute that for boundary violations. That should almost eliminate any chance of an EnemyTank instance getting trapped in a corner. In addition, we’ll add tank-versus-tank collision detection and shell-versus-tank collision dectection. Tank versus tank will result in the destruction of both tanks, and shells will do damage to tanks until a maxHits parameter is reached, triggering tank destruction. We’ll add a DestroySound sound and Explosion MovieClip to play when a tank is destroyed. We’ll also add the stats display and the game-over view. That will require updates to the FLA and every class except KBcontrol and IntroView. Version _08 Version _08 Main.as package com.kglad { import flash.display.MovieClip; 149
150 Chapter 6 n Developing a Flash Game import flash.events.Event; public class Main extends MovieClip { private var introView:IntroView; private var combatView:CombatView; private var gameOverView:GameOverView; public function Main() { addIntroViewF(); } private function addIntroViewF():void{ introView = new IntroView(); introView.addEventListener("startCombatE",addCombatViewF); addChild(introView); } private function removeIntroViewF():void{ // Ready intoView for gc introView.removeEventListener("startCombatE",addCombatViewF); removeChild(introView); introView = null; } private function addCombatViewF(e:Event):void{ removeIntroViewF(); // Add a CombatView instance combatView = new CombatView(); combatView.addEventListener("gameOverE",addGameOverViewF); addChild(combatView); } private function removeCombatViewF():void{ // Ready combatView for gc combatView.removeEventListener("gameOverE",addGameOverViewF); removeChild(combatView); combatView = null; } private function addGameOverViewF(e:Event):void{ // I wanted to get this player reference // before losing my reference to combatView, // so I could pass it to the GameOverView // instance. Then I could display player stats // in the GameOverView instance. var player:PlayerTank = combatView.playerT; removeCombatViewF(); // Add a GameOverView instance gameOverView = new GameOverView(player); // Add a listener for the replay button gameOverView.addEventListener("replayE",replayF);
Version _08 addChild(gameOverView); } private function removeGameOverViewF():void{ // Ready gameOverView for gc gameOverView.removeEventListener("replayE",replayF); removeChild(gameOverView); gameOverView = null; } private function replayF(e:Event):void{ removeGameOverViewF(); // And add an IntroView instance. The customized // keys do not need to be redefined. addIntroViewF(); } } } Version _08 CombatView.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.utils.Timer; import flash.events.TimerEvent; public class CombatView extends MovieClip { private var player:PlayerTank; // Option to control the number of enemy tanks private var enemyNum:int = 11; private var enemyA:Array = []; // maxHits defined in Tank and passed from Tank private var maxHits:int; // Time in ms to delay removing this view and // adding game over view private var timeTilGameOverView:int = 3000; public function CombatView() { this.addEventListener(Event.ADDED_TO_STAGE,init); this.addEventListener(Event.REMOVED_FROM_STAGE,removedF); } private function init(e:Event):void{ this.removeEventListener(Event.ADDED_TO_STAGE,init); statsDisplayF(); // Add player tank and enemy tank(s) addTanksF(); } private function statsDisplayF():void{ 151
152 Chapter 6 n Developing a Flash Game // Initialize the display of player score // and player life score_tf.text = "0"; life_tf.text = "100%"; } private function addTanksF():void{ var w:int = arena_mc.width; var h:int = arena_mc.height player = new PlayerTank(w,h,this); // I needed Tank instance names to distinguish // one EnemyTank from another so I may as well // assign a name property to the PlayerTank // instance and use it. I could distinguish the // PlayerTank instance by using if(tank is // PlayerTank) but there was no way to distinguish // one EnemyTank from another EnemyTank by checking // Class membership. player.name = "player"; // Create arrays for each Tank instance to store // their foes (foeA) and friends (friendA). // Arrays created in Tank. Used to determine who // to shoot and who to not shoot for fear // of hitting a friend. Some of that is done in // Tank but, as yet, there is no code to withdraw // a shot that is apt to hit a friend. positionF(arena_mc,player); // Add enemy tank(s) for(var i:int=0;i<enemyNum;i++){ // Pass a reference to "this" instance so // EnemyTank instances have access to the // getter below var enemy:EnemyTank = new EnemyTank(w,h,this); // The name property is a string so I have // to coerce the int i to a string. You can // also use String(i). enemy.name = i.toString(); // All enemies have player as a foe. You can // also add other enemies and have a free-for// all where every Tank instance shoots every // other Tank instance. enemy.foeA.push(player); positionF(arena_mc,enemy); enemyA.push(enemy); }
Version _08 // all enemies are player foes. player.foeA = player.foeA.concat(enemyA); // player has no friends. So, I did not define a // player.friendA. You’re on your own. If you want // a free-for-all (enemies shooting each other and // player) - uncomment this for loop and comment // out the next for loop ///* for(i=0;i<enemyNum;i++){ // Add all enemies to each enemy’s foeA enemyA[i].foeA = enemyA[i].foeA.concat(enemyA); // Remove enemyA[i] so no one is their own foe. enemyA[i].foeA.splice(i+1,1); } //*/ // The next for loop makes enemy instances all friends // of each other. But, if you want a free-for-all, comment // out the following for loop and uncomment the above for loop. /* for(i=0;i<enemyNum;i++){ // add all enemies to each enemy’s friendA enemyA[i].friendA = enemyA[i].friendA.concat(enemyA); // remove enemyA[i] so no one is their own friend. // At this point no one targets friends, but if a // shell hits a friend, it counts. Later I may add // code to check that no shot is apt to hit a friend. enemyA[i].friendA.splice(i,1); } */ } private function positionF(arena_mc:MovieClip,mc:MovieClip):void { arena_mc.addChild(mc); mc.x = int(mc.width+(arena_mc.width-2*mc.width)*Math.random()); mc.y = int(mc.height+(arena_mc.height-2*mc.height)*Math.random()); // If this is an enemy tank make sure it’s not hitting // another tank. player is added first so no need to // check for a hit when it’s added. if(mc!=player){ if(player.hitTestObject(mc)){ positionF(arena_mc,mc); } else { // check mc doesn’t hit any enemyA element for(var i:int=0;i<enemyA.length;i++){ if(mc.hitTestObject(enemyA[i])){ 153
154 Chapter 6 n Developing a Flash Game positionF(arena_mc,mc); break; } } } } } ///// begin data sharing code //// // Allow Tank to access player public function get playerT():PlayerTank{ return player; } // Pass maxHits from Tank (where it is a parameter) so it // can be used in the stats display (in playerHitF below) public function maxHitsF(n:int):void{ maxHits = n; } // Pass a combat start delay parameter so the stats display // (ie, combat countdown) can be updated, warning player how // long before combat starts. I needed this delay when I // started to allow multiple enemies. Without it, I (player) // was getting creamed before I had a chance to move. public function startTimeF(n:int):void{ // Display time until start startTime_tf.text = n.toString(); // Create timer to countdown the start time. var startTimer:Timer = new Timer(1000,n); startTimer.addEventListener(TimerEvent.TIMER,timerF); startTimer.start(); } // Destroyed tank (from Tank) is passed here so it can be // removed from all arrays: enemyA, player.foeA, all // enemyA member.foeA and all enemyA member.friendA public function removeTankF(tank:Tank):void{ // Remove if a player foe. for(var j2:int=player.foeA.length-1;j2>=0;j2– –){ if(player.foeA[j2].name==tank.name){ player.foeA.splice(j2,1); break; } } // Remove from enemyA before check enemyA element // foeA and friendA. No need to look for tank in // its own foeA or enemyA
Version _08 for(j2=enemyA.length-1;j2>=0;j2– –){ if(enemyA[j2].name==tank.name){ enemyA.splice(j2,1); break; } } for(j2=enemyA.length-1;j2>=0;j2– –){ // Remove if an enemy foe for(var j3:int=enemyA[j2].foeA.length-1;j3>=0;j3– –){ if(enemyA[j2].foeA[j3].name==tank.name){ enemyA[j2].foeA.splice(j3,1); break; } } // Remove if an enemy friend for(j3=enemyA[j2].friendA.length-1;j3>=0;j3– –){ if(enemyA[j2].friendA[j3].name==tank.name){ enemyA[j2].friendA.splice(j3,1); break; } } } } // score is passed from Tank to update the stats display public function set score(n:int):void{ // I need to pass something to GameOverView so it // can display some stats and determine if player // won or lost. I used a player reference and // tacked a new property onto player (score), which // continues my ad-hoc data handling. I used so much // ad-hoc data handling, without comments, even I // can’t remember where data is being sent or received. // I should fix that by using one class for handling data // like I did with the keyboard control data. // In fact, that would be a good update. I will rename // KBcontrol to something clever (like Data) and pass and // retrieve data that needs to be shared across classes // via a Data class. But for now, I assigned a property // to player so that property can be used in GameOverView. player.score = n; // Update stats display score_tf.text = n.toString(); if(enemyA.length == 0){ 155
156 Chapter 6 n Developing a Flash Game // If there are no more enemies, the game is over // and player won. Well, there’s actually a // possible bug here (that I will need to fix // later) because in free-for-all mode, when // player is dead, EnemyTank instances keep // shooting each other because I don’t // instantly close down the CombatView // instance. (More on that below.) This // function is where I delay removing this // view and adding game over view. delayLeavingCombatViewF(); } } // This is called from Tank to update the stats display public function playerHitF():void{ // This is where maxHits is used to update the // stats display life_tf.text = int(100*(maxHits-player.hits)/maxHits)+"%"; if(player.hits==maxHits){ // Game over. player lost. // Same delay function executes whether // player wins or loses. delayLeavingCombatViewF(); } } //////// end data sharing code ////// private function timerF(e:TimerEvent):void{ // Update start time countdown startTime_tf.text = (e.target.repeatCount-e.target.currentCount).toString(); if(e.target.currentCount==e.target.repeatCount){ // Remove the listener when no longer needed. e.target.removeEventListener(TimerEvent.TIMER,timerF); } } private function delayLeavingCombatViewF():void{ // I use the timeTilGameOverView variable here and // call endCombatF() when it is time to remove this // CombatView var delayTimer:Timer = new Timer(timeTilGameOverView,1); delayTimer.addEventListener(TimerEvent.TIMER,endCombatF); delayTimer.start(); } // Finally, dispatch the endCombatE event so Main can
Version _08 // remove the CombatView instance and add a GameOverView // instance. private function endCombatF(e:Event):void{ e.target.removeEventListener(TimerEvent.TIMER,endCombatF); dispatchEvent(new Event("endCombatE")); } private function removedF(e:Event):void{ this.removeEventListener(Event.REMOVED_FROM_STAGE,removedF); for(var i:int=0;i<arena_mc.numChildren;i++){ arena_mc.removeChildAt(0); } // empty enemyA enemyA.length = 0; player = null; // There should be no more tank references, now } } } Version _08 GameOverView.as package com.kglad { import import import import import import public flash.display.MovieClip; flash.events.Event; flash.events.MouseEvent; flash.text.TextField; flash.text.TextFormat; flash.text.Font; class GameOverView extends MovieClip { // I use player to get data from CombatView. private var player:PlayerTank public function GameOverView(_player:PlayerTank) { // This is really a terrible way to pass data. // The more I see, the more aggravated I get. // If I use arrows to show where the player // reference is coming from and going to, it // would look like: // CombatView -> Main -> GameOverView // And some of the data is needed in GameOverView // and attached to player (player.score) for no // reason other than to get the data into // GameOverView and follows this trail: // Tank -> CombatView // So, this is confirmation, if any is needed, that 157
158 Chapter 6 n Developing a Flash Game // data handling needs to be cleaned up. It is too // convoluted and practically Byzantine. player = _player; this.addEventListener(Event.ADDED_TO_STAGE,init); this.addEventListener(Event.REMOVED_FROM_STAGE,removedF); listenersF(); } private function init(e:Event):void{ this.removeEventListener(Event.ADDED_TO_STAGE,init); // Create the start of the string that will be // displayed in this view’s textfield. if(player.foeA.length==0){ var s:String = "You won!!\n\nYou had "; } else { s = "You lost!\n\nYou had "; } // Append the rest of the string. Here is where // I use player to get data into GameOverView s+= player.score+" enemy hits\nand you were shot "+player.hits+" times."; // I’m going to add a textfield and format its // text so it uses the same font that’s been // used in the static text. Consequently, I want // to embed the font to ensure users see what // I see and don’t use a system font if they don’t // have Verdana regular. This is how to embed a font // for a textfield created with ActionScript. var tfor:TextFormat = new TextFormat(); // I added Verdana regular font to my library and // assigned a suggestive class name, Verdana. // Adding a font to your library is a multistep // process. Click the upper right of your library // panel > click "New Font…" > type a name > // select your preferred Family and Style from the // combo boxes > check the needed "Character // ranges" > click ActionScript > check "Export // for ActionScript" > type a Class name > // click OK. If the compiler complains about // trying to access fontName from verdana, // type it as a Font, not Verdana: // var verdana:Font = new Verdana(); // or cast verdana as a font: // Font(verdana).fontName var verdana:Verdana = new Verdana();
Version _08 // Other than adding a font to the library, this is // the only non-obvious step. The textformat’s font // should be assigned the fontName property of your font. tfor.font = verdana.fontName; tfor.size = 14; // Create the textfield var tf:TextField = new TextField(); // Enable the textfield’s embedFonts property. tf.embedFonts = true; // Assign text tf.text = s; // If text will be assigned after assigning the // textformat, use the defaultTextFormat property // of tf: // tf.defaultTextFormat = tfor; // If the text is assigned before the textformat is // applied, use the setTextFormat() method: tf.setTextFormat(tfor); // And, if needed (or you do not understand when to // use one or the other), you can do both. tf.multiline = true; tf.width = 400; tf.autoSize = "left"; tf.x = (stage.stageWidth-tf.width)/2; tf.y = 200; addChild(tf); } private function listenersF():void{ replay_mc.addEventListener(MouseEvent.CLICK,replayF); replay_mc.buttonMode = true; } private function removedF(e:Event):void{ for(var i:int=0;i<this.numChildren;i++){ this.removeChildAt(0); } this.removeEventListener(Event.REMOVED_FROM_STAGE,removedF); } private function replayF(e:Event):void{ replay_mc.removeEventListener(MouseEvent.CLICK,replayF); dispatchEvent(new Event("replayE")); } } } 159
160 Chapter 6 n Developing a Flash Game Version _08 Tank.as package com.kglad { import flash.display.MovieClip; import flash.media.Sound; import flash.events.Event; import flash.events.MouseEvent; import flash.utils.getTimer; public class Tank extends MovieClip{ private var xLimit:int; private var yLimit:int; protected var prevX:Number; protected var prevY:Number; private var shellA:Array = []; private var gunL:Number; private var shellSpeed:int = 10; private var shotSound:Sound = new ShotSound(); private var destroySound:Sound = new DestroySound(); // Number of foes shot. private var scoreShots:int = 0; // I decided to limit the maximum number of shells // that can exist on-screen at any one time so this // is not a blast-fest. private var maxShots:int = 1; // Number of hits until tank destruction. Needed // internal because I wanted to use for stats in // CombatView internal var maxHits:int = 2 ; // Number of incoming shell hits. Needed internal // because I wanted to use for stats in CombatView internal var hits:int; // array of foes internal var foeA:Array = []; // array of friends internal var friendA:Array = []; // Shell hit something (so it cannot hit anything // else and must be removed) private var shellHit:Boolean; // A reference to the CombatView instance so Tank can // exchange some data with CombatView private var cv:MovieClip; // Seconds delay until shooting allowed. I was getting // creamed at the start (especially when there were more // than a few enemies) so I added a shooting delay. That
Version _08 // lets me get into a better defensive position when the // firing begins. private var startTime:int = 3; // Used for the start shooting delay private var initTime:int; // Here’s d2r defined once and used in Tank, PlayerTank // and EnemyTank protected const d2r:Number = Math.PI/180; public function Tank(_xLimit:int,_yLimit:int,_view:MovieClip=null) { // This called from IntroView where no _view parameter // is needed and none is passed. If a _view parameter is // passed it is from CombatView and then it is needed because // of my ridiculous data handling "scheme". (I actually don’t // think the data handling exemplified in this code deserves // to be called a scheme. It is a hodgepodge of devices.) if(_view){ cv = _view; } // Used in CombatView for stats display. if(cv){ // send maxHits and the start delay to the // CombatView instance for display purposes // (the player life remaining and start time // countdown, resp.). cv.maxHitsF(maxHits); cv.startTimeF(startTime); } xLimit = _xLimit; yLimit = _yLimit; this.addEventListener(Event.ADDED_TO_STAGE,initT); this.addEventListener(Event.REMOVED_FROM_STAGE, removedF); } private function initT(e:Event):void{ // Start a loop to check for tank vs tank collisions this.addEventListener(Event.ENTER_FRAME,tank_v_tankCollisionsF); this.removeEventListener(Event.ADDED_TO_STAGE,initT); // initTime use to ensure no tank starts firing for // first few seconds initTime = getTimer()+startTime*1000; prevX = this.x; prevY = this.y; // Initialize number of times tank hit by incoming hits = 0; gunL = this.turret_mc.width-this.turret_mc.top_mc.width/2; 161
162 Chapter 6 n Developing a Flash Game } private function tank_v_tankCollisionsF(e:Event):void{ // Collisions destroy both tanks. I am assuming all // tanks (except self) are in foeA or enemyA. At least, // those (in foeA and friendA) are the only collisions // checked. I use destroyedBool so I don’t check for // collisions after one has been detected. var destroyedBool:Boolean = tank_v_someTanksF(foeA); // If no foe collision, check for friend collision. if(!destroyedBool){ // Check for this vs friend collision tank_v_someTanksF(friendA); } } // ffA = friend/foe Array private function tank_v_someTanksF(ffA:Array):Boolean{ for(var i:int=0;i<ffA.length;i++){ // Check if there’s a tank base_mc colliding // with another tank base_mc. if(this.base_mc.hitTestObject(ffA[i].base_mc)){ // If so, destroy them both. Done // in destroyF() destroyF(foeA[i]); destroyF(this); // Set this Boolean so the friendA // isn’t checked return true; } } return false; } protected function shootF(e:MouseEvent):void{ // No shots allowed unless number of shells ok, // there’s a foe and it’s time to start if(shellA.length<maxShots && foeA.length>0 && getTimer()>initTime){ shotSound.play(); var shell:Shell = new Shell(); shellA.push(shell); this.parent.addChild(shell); shell.cos = Math.cos((this.turret_mc.rotation+this.rotation)*d2r); shell.sin = Math.sin((this.turret_mc.rotation+this.rotation)*d2r);
Version _08 // shell’s initial position shell.tankX = this.x; shell.tankY = this.y; // I am going to increment f (by shellSpeed) // in each loop to determine the distance // traveled by the shell. shell.f = gunL; shell.x = this.x+gunL*shell.cos; shell.y = this.y+gunL*shell.sin; if(shellA.length==1){ this.addEventListener(Event.ENTER_FRAME,shellLoopF); } } } private function shellLoopF(e:Event):void{ for(var i:int=shellA.length-1;i>=0;i– –){ shellHit = false; // Update the distance the shell has traveled shellA[i].f+=shellSpeed; // Assign its x,y using the parameters defined in shootF() shellA[i].x = shellA[i].tankX+shellA[i].f*shellA[i].cos; shellA[i].y = shellA[i].tankY+shellA[i].f*shellA[i].sin; // Check for hit vs foes hitF(foeA,i); // If a foe hit and this is player, display stat if(shellHit && this.name == "player"){ scoreShots++; cv.score = scoreShots; } else { // If this is not player, refresh player // hits - it’s possible player’s been the // one hit. if(this.name!="player"){ // cv.playerHitsF() does not // increment player hits // (because it may not have // been hit), it just triggers // cv to check player hits and // update the display cv.playerHitF(); } } if(!shellHit){ // Check for hit vs friends 163
164 Chapter 6 n Developing a Flash Game hitF(friendA,i); } // If shell not removed because of a hit, // check for boundary violation. if(!shellHit){ if(boundaryViolationF(shellA[i])){ shellRemoveF(i); } } } } // ffA = friend/foe Array private function hitF(ffA:Array,shellIndex:int):void{ for(var j:int=ffA.length-1;j>=0;j– –){ // Check for shell hit if(ffA[j].hitTestObject(shellA[shellIndex])){ // If a hit, assign shellHit shellHit = true; // Remove the shell shellRemoveF(shellIndex); // Update ffA[j]’s hits ffA[j].hits++; // Check if ffA[j] has reached // maxHits limit if(ffA[j].hits>=maxHits){ // ffA[j] has been destroyed destroyF(ffA[j]); } break; } } } private function destroyF(tank:Tank):void{ destroySound.play(); // Add explosion. var explosion:Explosion = new Explosion(); // Listen for last frame to play in explosion // (where a "removeE" event is dispatched). explosion.addEventListener("removeE",removeExplosionF); trace(tank.parent); tank.parent.addChild(explosion); // Position explosion where tank was located explosion.x = tank.x; explosion.y = tank.y;
Version _08 tank.parent.removeChild(tank); // Pass tank to cv where it should be removed // from all arrays. cv.removeTankF(tank); if(tank.name=="player"){ // May have been destroyed by tank collision, // which is no big deal unless tank is player. // In that case, player will be destroyed but // the stats will show life remaining. This // will prevent that issue. tank.hits = maxHits; cv.playerHitF() } } private function shellRemoveF(i:int):void{ // ready shellA[i] for gc this.parent.removeChild(shellA[i]); shellA.splice(i,1); if(shellA.length==0){ this.removeEventListener(Event.ENTER_FRAME,shellLoopF); } } protected function moveLimitsF():void{ if(boundaryViolationF(this)){ this.x = prevX; this.y = prevY; } } // I changed this to be consistent with the notation // in EnemyTank and decided this is needed because I // saw a boundary problem while testing other code. protected function boundaryViolationF(mc:MovieClip):String{ var b:Boolean = false; if(mc.x<mc.width/2){ b=true; } else if(mc.x>xLimit-mc.width/2){ b=true; } else if(mc.y<mc.height/2){ b=true; } else if(mc.y>yLimit-mc.height/2){ b=true; } // If there’s a boundary violation, return the tank’s 165
166 Chapter 6 n Developing a Flash Game // quadrant, so if this is an EnemyTank, it can turn // toward stage center after moving backward. if(b){ return quadrantF(); } else { return ""; } } protected function quadrantF():String{ var quadrant:String; if(this.x<this.parent.width/2){ quadrant="L"; } else { quadrant="R"; } if(this.y<this.parent.height/2){ quadrant+="U"; } else { quadrant+="D"; } return quadrant; } private function removeExplosionF(e:Event):void{ e.currentTarget.removeEventListener("removeE",removeExplosionF); // Sometimes an explosion’s parent is null because // it is not on-stage. When that happens, applying // removeChild() to it will trigger a null object // reference error. This if statement prevents that // null object error if(e.currentTarget.stage){ e.currentTarget.parent.removeChild(e.currentTarget); } } private function removedF(e:Event):void{ this.removeEventListener(Event.ENTER_FRAME,tank_v_tankCollisionsF); this.removeEventListener(Event.REMOVED_FROM_STAGE,removedF); if(shellA.length>0){ this.removeEventListener(Event.ENTER_FRAME,shellLoopF); for(var i:int=shellA.length-1;i>=0;i– –){ if(shellA[i].parent){ shellA[i].parent.removeChild(shellA[i]); } } }
Version _08 } } } Version _08 PlayerTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.ui.Mouse; public class PlayerTank extends Tank { private var speed:int = 3; private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; private var keyCodeIndex:int; private var directionA:Array = []; private var cCursor:CustomCursor; // I added a score variable so player could use this // to pass data to GameOverView public var score:int; public function PlayerTank(_xLimit:int,_yLimit:int,_cv:MovieClip=null) { // pass the CombatView instance to Tank super(_xLimit,_yLimit,_cv); cCursor = new CustomCursor(); this.addEventListener(Event.ADDED_TO_STAGE,initP); this.addEventListener(Event.REMOVED_FROM_STAGE,removedP); } private function initP(e:Event):void{ // no change } private function addCustomCursorF(e:MouseEvent):void{ // no change } private function removeCustomCursorF(e:MouseEvent):void{ // no change } private function rotateTurretF(e:Event):void{ // if statement used to prevent null object error if(cCursor && this.parent){ cCursor.x = this.parent.mouseX; cCursor.y = this.parent.mouseY; 167
168 Chapter 6 n Developing a Flash Game // Using d2r from Tank this.turret_mc.rotation = -this.rotation+Math. atan2(this.parent.mouseY-this.y,this.parent.mouseX-this.x)/d2r; } } private function keydownF(e:KeyboardEvent):void{ // no change } private function keyupF(e:KeyboardEvent):void{ // no change } private function moveTankF(e:Event):void{ // no change } private function removedP(e:Event):void{ // Remove the custom cursor if it’s on-stage when // player removed and show the mouse if(cCursor.stage){ Mouse.show(); this.parent.removeChild(cCursor); } // null the custom cursor so it can be gc’d // (although, once this instance is gc’d, custom // cursor should be gc’d) cCursor = null; this.removeEventListener(Event.REMOVED_FROM_STAGE,removedP); this.parent.removeEventListener(MouseEvent.MOUSE_DOWN, shootF); this.parent.removeEventListener(MouseEvent.ROLL_OVER, addCustomCursorF); this.parent.removeEventListener(MouseEvent.ROLL_OUT, removeCustomCursorF); stage.removeEventListener(KeyboardEvent.KEY_DOWN,keydownF); stage.removeEventListener(KeyboardEvent.KEY_UP,keyupF); this.removeEventListener(Event.ENTER_FRAME,moveTankF); this.removeEventListener(Event.ENTER_FRAME,rotateTurretF); } } }
Version _08 Version _08 EnemyTank.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.geom.ColorTransform; import flash.utils.Timer; import flash.events.TimerEvent; public class EnemyTank extends Tank { private var speed:int = 3; private var rotationRate:int = 3; private var xLimit:int; private var yLimit:int; private var cv:CombatView; private var currentDirection:int; private var nextDirection:int; private var distance:int; private var startX:int; private var startY:int; private var directionChangeFreq:int = 2500; private var directionChangeTimer:Timer; private var moveBackBool:Boolean; private var moveBackTimer:Timer; // In case I add an EnemyTank to a non-CombatView, I coded // a default _cv here. But it’s not currently used. public function EnemyTank(_xLimit:int,_yLimit:int,_cv:CombatView=null) { super(_xLimit,_yLimit,_cv); colorF(); cv = _cv; currentDirection = int(360*Math.random()); distance = speed; this.addEventListener(Event.REMOVED_FROM_STAGE,removedF); this.addEventListener(Event.ENTER_FRAME,rotateTurretF); this.addEventListener(Event.ENTER_FRAME,moveTankF); directionChangeTimer = new Timer(directionChangeFreq,0); directionChangeTimer.addEventListener(TimerEvent.TIMER, newDirectionF); directionChangeTimer.start(); moveBackTimer = new Timer(1000,1); moveBackTimer.addEventListener(TimerEvent.TIMER,moveBackF); } 169
170 Chapter 6 n Developing a Flash Game private function newDirectionF(e:TimerEvent):void{ // ai: turn towards stage center. quadrantF in Tank var quadrant:String = quadrantF(); // No sense writing the same code twice here and // in moveLimitsF turnF(quadrant); this.addEventListener(Event.ENTER_FRAME,directionChangeF); } private function colorF():void{ // no change } private function rotateTurretF(e:Event):void{ // Shoot at closest foe. This is for // free-for-all mode. var closestFoe:Tank = closestFoeF(); // If there’s a foe, there’s a closest foe, // but there may not be any foes remaining. if(closestFoe){ // Update turret rotation, then shoot. // Use d2r variable this.turret_mc.rotation = this.rotation+Math.atan2(closestFoe.y-this.y,closestFoe.x-this.x)/d2r; shootF(null); } } private function closestFoeF():Tank{ // Find the closest foe. Because dist1*dist1<dist2*dist2 // implies dist1<dist2, I can save more cpu cycles by // using the distance squared instead of the actual // distance, which would require using Math.sqrt(). // Initialize closestDist to be equal to the stage’s // diagonal length squared var closestDist:Number = stage.stageWidth* stage.stageWidth+stage.stageHeight*stage.stageHeight; // Initialize closestFoe var closestFoe:Tank; // Loop through the foes for(var i:int=0;i<this.foeA.length;i++){ // Find the distance (squared) from "this" tank // to foeA[i] var dist:Number = distF(this.foeA[i]); // If that is less than closestDist, update // closestDist and closestFoe. if(dist<closestDist){
Version _08 closestDist = dist; closestFoe = this.foeA[i]; } } // Return the victim to be shot (at). return closestFoe; } private function distF(t:Tank):Number{ // Math.sqrt() is actual distance, but we just // need the relative distance. return (this.x-t.x)*(this.x-t.x)+(this.y-t.y)*(this.y-t.y); } private function moveTankF(e:Event):void{ // Another if statement to prevent null object errors if(this.stage){ if(startX==0){ startX = this.x; startY = this.y; } if(!moveBackBool){ this.rotation = currentDirection; } this.x = startX+distance*Math.cos(currentDirection*d2r); this.y = startY+distance*Math.sin(currentDirection*d2r); distance += speed; moveLimitsF(); prevX = this.x; prevY = this.y; } } override protected function moveLimitsF():void{ var s:String = boundaryViolationF(this); // If there is a boundary violation, move backwards if(s){ this.x = prevX; this.y = prevY; // Move in opposite direction directionChangeTimer.stop(); directionChangeUpdateF(); currentDirection = (180+currentDirection)%360; moveBackBool = true; moveBackTimer.start(); this.removeEventListener(Event.ENTER_FRAME,directionChangeF); 171
172 Chapter 6 n Developing a Flash Game // In this version nextDirection is chosen // more wisely. Rather than turn -90 degrees // like the previous version, this tank will // now turn toward stage center. turnF(s); } } private function turnF(quadrant:String):void{ if(quadrant=="LU"){ nextDirection = int(90*Math.random()); } else if(quadrant=="LD"){ nextDirection = 270+int(90*Math.random()); } else if(quadrant=="RU"){ nextDirection = 90+int(90*Math.random()); } else { nextDirection = 180+int(90*Math.random()); } } private function moveBackF(e:TimerEvent):void{ // no change } private function directionChangeF(e:Event):void{ // no change } private function directionChangeUpdateF():void{ // no change } private function removedF(e:Event):void{ // no change } } } This works well, so we’re nearing the end of the work on this phase of the tank combat game. Obviously (from my comments), we need to organize the data handling. While we’re organizing the data so it’s all in our Data class (renamed and expanded from KBcontrol), we’re adding options for the user to control a number of parameters from the IntroView class. Although I’m not a fan of Flash components, I decided to use the slider to speed finishing this phase of the work. Typically, we would copy the graphics (like we did for the check box in version _09) and write our own code for handling the object’s functionality. I thought we would encounter problems that would justify my
Version _09 comment about trying to avoid using components, but I found no problems using the slider, so I left it in version _09. There were changes in many of the files. In particular, the FLA has quite a few changes to the _intro view symbol to allow the control of many more parameters than just the keyboard control keys, and the old KBcontrol class (now the Data class) has been expanded to include all those parameters. There were no changes in PlayerTank or EnemyTank. Version _09 Version _09 Main.as public function Main() { // The first line of code in the constructor is one // of the two things new in Main. // Initialize all the new IntroView parameters to // their default values. Check the static resetAllF() // function in Data to see how this works. Data.resetAllF(); addIntroViewF(); } // The other change was adding a "replayE" event listener and // its related code to remove the listener and code for the // listener function. private function addGameOverViewF(e:Event):void{ removeCombatViewF(); // Add a GameOverView instance gameOverView = new GameOverView(); // Add a listener for the replay button gameOverView.addEventListener("replayE",replayF); addChild(gameOverView); } private function removeGameOverViewF():void{ // Ready gameOverView for gc gameOverView.removeEventListener("replayE",replayF); removeChild(gameOverView); gameOverView = null; } private function replayF(e:Event):void{ removeGameOverViewF(); // And add an IntroView instance. addIntroViewF(); } 173
174 Chapter 6 n Developing a Flash Game Version _09 IntroView.as package com.kglad { import flash.display.MovieClip; import flash.events.MouseEvent; import flash.events.KeyboardEvent; import flash.events.Event; public class IntroView extends MovieClip { // An array of all MovieClips that contain a slider, // defined in init(). Used to ease coding for the slider // mouse listeners and for assigning the initial values // of the sliders. private var mc_sliderA:Array; public function IntroView() { this.addEventListener(Event.ADDED_TO_STAGE,init); // Clean up everything in this class when no longer needed. this.addEventListener(Event.REMOVED_FROM_STAGE,removeF); } private function init(e:Event):void{ this.removeEventListener(Event.ADDED_TO_STAGE,init); mc_sliderA = [tankSpeed_mc,rotationRate_mc,shellSpeed_mc,maxShells_mc,maxHits_mc,enemyNum_mc, enemyEvasiveness_mc,startTime_mc,combatEndDelay_mc]; listenersF(); addPlayerTankF(); } private function removeF(e:Event):void{ this.removeEventListener(Event.REMOVED_FROM_STAGE,removeF); // Remove all the listeners created here removeListenersF(); // Remove everything including that problematic // startCombat_mc button that had focus and was // then removed from the stage. removeAllF(); } // add listeners and assign default values private function listenersF():void{ left_mc.addEventListener(MouseEvent.CLICK,leftF); left_mc.buttonMode = true; right_mc.addEventListener(MouseEvent.CLICK,rightF); right_mc.buttonMode = true; forward_mc.addEventListener(MouseEvent.CLICK,forwardF); forward_mc.buttonMode = true; back_mc.addEventListener(MouseEvent.CLICK,backF);
Version _09 back_mc.buttonMode = true; startCombat_mc.addEventListener(MouseEvent.CLICK,startCombatF); startCombat_mc.buttonMode = true; // Slider listeners for(var i:int=0;i<mc_sliderA.length;i++){ // I wanted to supply an explanation of what // each slider controls and I don’t have enough // room in the _intro view symbol to use static // text. So, I added a MouseEvent.MOUSE_OVER // event to cause explanatory text to // appear in a dynamic TextField. mc_sliderA[i].addEventListener(MouseEvent.MOUSE_OVER,explanationF); // I started with a SliderEvent.CHANGE event but // it updates the value only after releasing the // slider, not as it is being changed. So, I // opted for a MouseEvent.MOUSE_MOVE event. mc_sliderA[i].sl.addEventListener(MouseEvent.MOUSE_MOVE,sliderChangeF); // Setting the initial values for the sliders. // The first time this page appears, those are // the default values (listed in Data). Thereafter, // the previously set values are used (just like the // tank key controls). setValuesF(i); } // Coding for the free-for-all MovieClip button. freeForAll_mc.cbox.addEventListener(MouseEvent.CLICK,freeForAllF); freeForAll_mc.addEventListener(MouseEvent.MOUSE_OVER,freeForAllExplanationF); setValuesF(-1); // Coding for the reset button that resets parameters // to default values. reset_mc.addEventListener(MouseEvent.CLICK,resetF); } private function removeListenersF():void{ left_mc.removeEventListener(MouseEvent.CLICK,leftF); right_mc.removeEventListener(MouseEvent.CLICK,rightF); forward_mc.removeEventListener(MouseEvent.CLICK,forwardF); back_mc.removeEventListener(MouseEvent.CLICK,backF); startCombat_mc.removeEventListener(MouseEvent.CLICK,startCombatF); // The most recently added listeners are listed here to make sure // I remove all the listeners. for(var i:int=0;i<mc_sliderA.length;i++){ mc_sliderA[i].removeEventListener(MouseEvent.MOUSE_OVER,explanationF); 175
176 Chapter 6 n Developing a Flash Game mc_sliderA[i].sl.removeEventListener(MouseEvent.MOUSE_MOVE,sliderChangeF); } freeForAll_mc.cbox.removeEventListener(MouseEvent.CLICK,freeForAllF); freeForAll_mc.removeEventListener(MouseEvent.MOUSE_OVER, freeForAllExplanationF); reset_mc.removeEventListener(MouseEvent.CLICK,resetF); } // Initial values for the sliders and free-for-all. private function setValuesF(i:int):void{ if(i>-1){ // Reminder: use the Flash API to find // Slider class properties (like value, // minimum and maximum) if you need help // understanding the slider code. // You should open Data.as to understand this. // Data.defaultVariableA is an array of the Data // variables listed as strings. I could use array // notation Data[Data.defaultVariableA[i]] to // access the variables in Data they were public // (or internal) variables. But they are not. So, I // use the getters, which are public. The getter name // is the same as the variable name without // the leading underscore. To remove the underscore // from those strings, I use split("_") and access // the 2nd array element (index 1). // For example, to get _tankSpeed in Data, I would // use Data.tankSpeed. Using array notation that // would be Data["tankSpeed"] = Data["_tankSpeed".split("_")[1]] = // Data[Data.defaultVariableA[1].split("_")[1]] mc_sliderA[i].sl.value = Data[Data.defaultVariableA[i+1].split("_")[1]]; mc_sliderA[i].tf.text = Data[Data.defaultVariableA[i+1].split("_")[1]]; } else { // freeForAll_mc.cbox has two frames. frame 1 // without the check mark, frame 2 with the // check mark. if(Data[Data.defaultVariableA[0].split("_")[1]]){ freeForAll_mc.cbox.gotoAndStop(2); } else { freeForAll_mc.cbox.gotoAndStop(1); }
Version _09 } } private function removeAllF():void{ for(var i:int=0;i<this.numChildren;i++){ removeChildAt(0); } } // This is where the text for the IntroView TextField (tf) // is assigned. private function explanationF(e:MouseEvent):void{ var val:int = e.currentTarget.sl.value switch(e.currentTarget.name){ case "tankSpeed_mc": tf.text = "Controls tank speed from a minimum of "+e.currentTarget.sl.minimum+" to a maximum of "+e.currentTarget.sl.maximum+"\nCurrent tank speed is "+val; break; case "rotationRate_mc": tf.text = "Controls tank turret rotation rate from a minimum of "+e.currentTarget.sl.minimum+" to a maximum of "+e.current Target.sl.maximum+"\nCurrent turret rotation rate is "+val; break; case "shellSpeed_mc": tf.text = "Controls tank shell speed from a minimum of "+e.currentTarget.sl.minimum+" to a maximum of "+e.currentTarget.sl.maximum+"\nCurrent tank shell speed is "+val; break; case "maxShells_mc": tf.text = "Controls maximum number of shells each tank can have on-screen at any one time. You can adjust from a minimum of "+e.currentTarget.sl.minimum+" to a maximum of "+e.currentTarget.sl.maximum+"\nCurrent tank shell maximum number is "+val; break; case "maxHits_mc": tf.text = "Controls number of shell hits each tank can withstand before destruction. You can adjust from a minimum of "+e.currentTarget.sl.minimum+" to a maximum of "+e.currentTarget.sl.maximum+"\nCurrent number of shell hits before destruction is "+val; break; case "enemyNum_mc": tf.text = "Controls the number of enemy tanks. You can adjust from a minimum of "+e.currentTarget.sl.minimum+" to a maximum of "+e.currentTarget.sl.maximum+"\nCurrent enemy number is "+val; break; 177
178 Chapter 6 n Developing a Flash Game case "enemyEvasiveness_mc": tf.text = "Controls the frequency of enemy tank evasive manuevers. You can adjust from a minimum of "+e.currentTarget.sl.minimum+" to a maximum of "+e.currentTarget.sl.maximum+"\nCurrent enemy tank evasiveness is "+val; break; case "startTime_mc": tf.text = "Controls the delay from the intial combat screen until the first shot is allowed. You can adjust from a minimum of "+e.currentTarget.sl.minimum+" second(s) to a maximum of "+e.currentTarget.sl.maximum+"\nCurrent delay is "+val; break; case "combatEndDelay_mc": tf.text = "Controls the delay from the end of combat until the End Game Screen. You can adjust from a minimum of "+e.currentTarget.sl.minimum+" second(s) to a maximum of "+e.currentTarget.sl.maximum+"\nCurrent delay is "+val; break; } } // Data class setters called when there is a slider change. private function sliderChangeF(e:Event):void{ var val:int = int(e.currentTarget.value); switch(e.currentTarget.parent.name){ case "tankSpeed_mc": tf.text = "Controls tank speed from a minimum of "+e.currentTarget.minimum+" to a maximum of "+e.currentTarget.maximum+"\nCurrent tank speed is "+val; Data.tankSpeed = val; e.currentTarget.parent.tf.text = val; break; case "rotationRate_mc": tf.text = "Controls tank turret rotation rate from a minimum of "+e.currentTarget.minimum+" to a maximum of"+e.currentTarget.maximum+" \nCurrent turret rotation rate is "+val; Data.rotationRate = val; e.currentTarget.parent.tf.text = val; break; case "shellSpeed_mc": tf.text = "Controls tank shell speed from a minimum of "+e.currentTarget.minimum+" to a maximum of "+e.currentTarget.maximum+"\nCurrent tank shell speed is "+val; Data.shellSpeed = val; e.currentTarget.parent.tf.text = val; break;
Version _09 case "maxShells_mc": tf.text = "Controls maximum number of shells each tank can have on-screen at any one time. You can adjust from a minimum of "+e.currentTarget.minimum+" to a maximum of "+e.currentTarget.maximum+"\nCurrent tank shell maximum number is "+val; Data.maxShells = val; e.currentTarget.parent.tf.text = val; break; case "maxHits_mc": tf.text = "Controls number of shell hits each tank can withstand before destruction. You can adjust from a minimum of "+e.currentTarget.minimum+" to a maximum of "+e.currentTarget.maximum+"\nCurrent number of shell hits before destruction is "+val; Data.maxHits = val; e.currentTarget.parent.tf.text = val; break; case "enemyNum_mc": tf.text = "Controls the number of enemy tanks. You can adjust from a minimum of "+e.currentTarget.minimum+" to a maximum of "+e.currentTarget.maximum+"\nCurrent enemy number is "+val; Data.enemyNum = val; e.currentTarget.parent.tf.text = val; break; case "enemyEvasiveness_mc": tf.text = "Controls the frequency of enemy tank evasive manuevers. You can adjust from a minimum of "+e.currentTarget.minimum+" to a maximum of "+e.currentTarget.maximum+"\nCurrent enemy tank evasiveness is "+val; Data.enemyEvasiveness = val; e.currentTarget.parent.tf.text = val; break; case "startTime_mc": tf.text = "Controls the delay from the start of the combat screen until the first shot is allowed. You can adjust from a minimum of "+e.currentTarget.minimum+" second(s) to a maximum of "+e.currentTarget.maximum+"\nCurrent delay is "+val; Data.startTime = val; e.currentTarget.parent.tf.text = val; break; case "combatEndDelay_mc": tf.text = "Controls the delay from the end of combat until the End Game Screen. You can adjust from a minimum of "+e.currentTarget.minimum+" second(s) to a maximum of "+e.currentTarget.maximum+"\nCurrent delay is "+val; Data.combatEndDelay = val; 179
180 Chapter 6 n Developing a Flash Game e.currentTarget.parent.tf.text = val; break; } } // freeForAll_mc.cbox clicked: private function freeForAllF(e:MouseEvent):void{ var mc:MovieClip = MovieClip(e.currentTarget); // If the check box is displayed frame 1 (unchecked), // goto frame 2 (checked) and update _freeForAll in // Data using the setter. Else, goto frame 1 and // update _freeForAll in Data using the setter. if(mc.currentFrame==1){ mc.gotoAndStop(2); Data.freeForAll = true; } else { mc.gotoAndStop(1); Data.freeForAll = false; } } // freeForAll_mc explanation. private function freeForAllExplanationF(e:MouseEvent):void{ tf.text = "Indicates whether combat is a free-for-all (all tanks fight each other) or not (it’s you against all enemy tanks)"; } // Reset all sliders and check box and parameters in Data private function resetF(e:MouseEvent):void{ Data.resetAllF(); for(var i:int=-1;i<mc_sliderA.length;i++){ setValuesF(i); } } // no change in any of the remaining functions private function addPlayerTankF():void{ } private function positionF(parent_mc:MovieClip,mc:MovieClip):void { } private function leftF(e:MouseEvent):void{ } private function rightF(e:MouseEvent):void{ } private function forwardF(e:MouseEvent):void{ } private function backF(e:MouseEvent):void{ }
Version _09 private } private } private } private } private } function leftKeyF(e:KeyboardEvent):void{ function rightKeyF(e:KeyboardEvent):void{ function forwardKeyF(e:KeyboardEvent):void{ function backKeyF(e:KeyboardEvent):void{ function startCombatF(e:MouseEvent):void{ } } Version _09 CombatView.as package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.utils.Timer; import flash.events.TimerEvent; public class CombatView extends MovieClip { private var player:PlayerTank; // Option to control the number of enemy tanks. Get via Data private var enemyNum:int = Data.enemyNum; private var enemyA:Array = []; // maxHits now get via Data private var maxHits:int = Data.maxHits; // Time in ms to delay removing this view and adding game // over view. // Get from Data private var timeTilGameOverView:int = 1000*Data.combatEndDelay; // Get from Data private var startTime:int = Data.startTime; public function CombatView() { // no change } private function init(e:Event):void{ // no change } private function statsDisplayF():void{ // no change } private function addTanksF():void{ var w:int = arena_mc.width; var h:int = arena_mc.height 181
182 Chapter 6 n Developing a Flash Game player = new PlayerTank(w,h); // Add listeners for events dispatched from Tank // to remove player from the arrays when destroyed, // register a hit to player and when player "scores" // or hits another tank player.addEventListener("removeTankE",removeTankF); player.addEventListener("playerHitE",playerHitF); player.addEventListener("scoreE",scoreF); // Get maxHits from Data maxHits = Data.maxHits; player.name = "player"; positionF(arena_mc,player); // Add enemy tank(s) for(var i:int=0;i<enemyNum;i++){ var enemy:EnemyTank = new EnemyTank(w,h); // Add listener for event dispatched from Tank // when an enemy is destroyed and should be // removed from arrays. enemy.addEventListener("removeTankE",removeTankF); enemy.name = i.toString(); enemy.foeA.push(player); positionF(arena_mc,enemy); enemyA.push(enemy); } // all enemies are player foes. player.foeA = player.foeA.concat(enemyA); // player has no friends. So, I did not define a // player.friendA. You’re on your own. There is // now a Data.freeForAll parameter used to indicate // a free-for-all if(Data.freeForAll){ for(i=0;i<enemyNum;i++){ // Add all enemies to each enemy’s foeA enemyA[i].foeA = enemyA[i].foeA.concat(enemyA); // Remove enemyA[i] so no one is their // own foe. enemyA[i].foeA.splice(i+1,1); } } else { for(i=0;i<enemyNum;i++){ // add all enemies to each enemy’s friendA enemyA[i].friendA = enemyA[i]. friendA.concat(enemyA); // remove enemyA[i] so no one is their own
Version _09 // friend. At this point, no one targets friends, // but if a shell hits a friend, it counts. Later // I may add code to check that no shot is apt to // hit a friend. enemyA[i].friendA.splice(i,1); } } // All tanks created. Start countdown to combat: startTimeF(startTime); } // I moved tanks further from the boundary. private function positionF(arena_mc:MovieClip,mc:MovieClip):void { arena_mc.addChild(mc); mc.x = int(2*mc.width+(arena_mc.width-4*mc.width)*Math. random()); mc.y = int(2*mc.height+(arena_mc.height-4*mc.height)*Math. random()); // If this is an enemy tank make sure it’s not hitting // another tank. player is added first so no need to // check for a hit when it’s added. if(mc!=player){ if(player.hitTestObject(mc)){ positionF(arena_mc,mc); } else { // check mc doesn’t hit any enemyA element for(var i:int=0;i<enemyA.length;i++){ if(mc.hitTestObject(enemyA[i])){ positionF(arena_mc,mc); break; } } } } } private function startTimeF(n:int):void{ // Display time until start startTime_tf.text = n.toString(); // Create timer to count down the start time // if startTime>0. if(n>0){ var startTimer:Timer = new Timer(1000,n); startTimer.addEventListener(TimerEvent.TIMER,timerF); startTimer.start(); } 183
184 Chapter 6 n Developing a Flash Game } // Event dispatched from Tank by destroyed tank uses // listener function here to call removeTankF. The // dispatching (= listening) tank is then removed from // all arrays: enemyA, player.foeA, all enemyA.foeA and // all enemyA.friendA public function removeTankF(e:Event):void{ var tank:Tank = Tank(e.currentTarget); tank.removeEventListener("removeTankE",removeTankF) // Remove if a player foe. for(var j2:int=player.foeA.length-1;j2>=0;j2–){ if(player.foeA[j2].name==tank.name){ player.foeA.splice(j2,1); break; } } // Remove from enemyA before check enemyA element // foeA and friendA. No need to look for tank in // its own foeA or enemyA for(j2=enemyA.length-1;j2>=0;j2–){ if(enemyA[j2].name==tank.name){ enemyA.splice(j2,1); break; } } // Check if enemyA.length==0. (Two enemy tanks may // have collided and could end combat.) if(enemyA.length==0){ playerHitF(); } for(j2=enemyA.length-1;j2>=0;j2–){ // Remove if an enemy foe for(var j3:int=enemyA[j2].foeA.length-1;j3>=0;j3–){ if(enemyA[j2].foeA[j3].name==tank.name){ enemyA[j2].foeA.splice(j3,1); break; } } // Remove if an enemy friend for(j3=enemyA[j2].friendA.length-1;j3>=0;j3–){ if(enemyA[j2].friendA[j3].name==tank.name){ enemyA[j2].friendA.splice(j3,1); break;
Version _09 } } } } // scoreF called via player scoreE event listener when // player scores a hit on an enemy tank private function scoreF(e:Event):void{ // Update stats display score_tf.text = Data.score.toString(); if(enemyA.length == 0){ // If there are no more enemies, the game is over // and player won. This function is where I delay // removing this view and adding game over view. delayLeavingCombatViewF(); } } // playerHitF called via player playerHitE event listener // and possibly from removeTankF() private function playerHitF(e:Event = null):void{ // This is where maxHits is used to update the // stats display life_tf.text = int(100*(maxHits-Data.playerHits)/maxHits)+"%"; if(Data.playerHits==maxHits){ // Game over. You lost. // Same delay function executes whether player // wins or loses. delayLeavingCombatViewF(); } } private function timerF(e:TimerEvent):void{ // Update start time countdown startTime_tf.text = (e.target.repeatCounte.target.currentCount).toString(); if(e.target.currentCount==e.target.repeatCount){ // Remove the listener when no longer needed. e.target.removeEventListener(TimerEvent.TIMER,timerF); } } private function delayLeavingCombatViewF():void{ // This fixes that problem where player could be // destroyed, but in free-for-all mode, enemy tanks // could keep destroying each other, leading to // possibly erroneous results being displayed in // the GameOverView instance. 185
186 Chapter 6 n Developing a Flash Game // Data.enemiesRemaining used in GameOverView // accurately indicating the number of enemies // left when player destroyed. Data.enemiesRemaining = enemyA.length; var delayTimer:Timer = new Timer(timeTilGameOverView,1); delayTimer.addEventListener(TimerEvent.TIMER,endCombatF); delayTimer.start(); } // Finally, dispatch the endCombatE event so main // can remove the CombatView instance and add a // GameOverView instance. private function endCombatF(e:Event):void{ e.target.removeEventListener(TimerEvent.TIMER,endCombatF); dispatchEvent(new Event("endCombatE")); } private function removedF(e:Event):void{ // Remove this event listener if(player.hasEventListener("playerHitE")){ player.removeEventListener("playerHitE",playerHitF); } // Remove this event listener if(player.hasEventListener("scoreE")){ player.removeEventListener("scoreE",scoreF); } this.removeEventListener(Event.REMOVED_FROM_STAGE,removedF); for(var i:int=0;i<arena_mc.numChildren;i++){ arena_mc.removeChildAt(0); } // empty enemyA enemyA.length = 0; player = null; // There should be no more tank references now } } } Version _09 GameOverView.as package com.kglad { import import import import import flash.display.MovieClip; flash.events.Event; flash.events.MouseEvent; flash.text.TextField; flash.text.TextFormat;
Version _09 import flash.text.Font; public class GameOverView extends MovieClip { public function GameOverView() { // no change } private function init(e:Event):void{ this.removeEventListener(Event.ADDED_TO_STAGE,init); // Using Data.enemiesRemaining, which is defined at // the end of combat, not when this class is // instantiated. if(Data.enemiesRemaining==0){ var s:String = "You won!!\n\nYou had "; } else { s = "You lost!\n\nYou had "; } s+= Data.score+" enemy hits\nand you were shot "+Data.player Hits+" times.\n\nThere were "+Data.enemiesRemaining+" enemies remaining."; var tfor:TextFormat = new TextFormat(); var verdana:Verdana = new Verdana(); tfor.font = verdana.fontName; tfor.size = 14; var tf:TextField = new TextField(); tf.embedFonts = true; tf.text = s; tf.setTextFormat(tfor); tf.multiline = true; tf.width = 400; tf.autoSize = "left"; tf.x = (stage.stageWidth-tf.width)/2; tf.y = 200; addChild(tf); } // replay_mc code private function listenersF():void{ replay_mc.addEventListener(MouseEvent.CLICK,replayF); replay_mc.buttonMode = true; } private function removedF(e:Event):void{ for(var i:int=0;i<this.numChildren;i++){ this.removeChildAt(0); } 187
188 Chapter 6 n Developing a Flash Game this.removeEventListener(Event.REMOVED_FROM_STAGE,removedF); } private function replayF(e:Event):void{ replay_mc.removeEventListener(MouseEvent.CLICK,replayF); dispatchEvent(new Event("replayE")); } } } Version _09 Tank package com.kglad { // The only changes are in the variable declarations: public class Tank extends MovieClip{ private var xLimit:int; private var yLimit:int; protected var prevX:Number; protected var prevY:Number; private var shellA:Array = []; private var gunL:Number; private var shellSpeed:int = Data.shellSpeed; private var shotSound:Sound = new ShotSound(); private var destroySound:Sound = new DestroySound(); // Number of foes shot. private var scoreShots:int = 0; // Get using Data private var maxShells:int = Data.maxShells; // Get using Data internal var maxHits:int = Data.maxHits; internal var hits:int; internal var foeA:Array = []; internal var friendA:Array = []; private var shellHit:Boolean; private var startTime:int = Data.startTime; // Used for the start shooting delay private var initTime:int; protected const d2r:Number = Math.PI/180; // I am set up to use different speeds and turret // rotation rates for PlayerTank and EnemyTank, but // I don’t think I’ll ever use that. // Two more data getters protected var speed:int = Data.tankSpeed; protected var rotationRate:int = Data.rotationRate;
Version _09 Version _09 Data package com.kglad { public class Data { // These are the default arrow-key keyCode’s private static var _forwardKeyCode:int = 38; private static var _backKeyCode:int = 40; private static var _leftKeyCode:int = 37; private static var _rightKeyCode:int = 39; // Variables with default values below private static var _freeForAll:Boolean; private static var _tankSpeed:int; private static var _rotationRate:int; private static var _shellSpeed:int; private static var _maxShells:int private static var _enemyNum:int; private static var _maxHits:int; private static var _enemyEvasiveness:int; private static var _startTime:int private static var _combatEndDelay:int; // Default values for above variables. Assigned // in resetAllF() below private static var defaultValueA:Array = [false,3,3,10,1,5,1,3,3,3]; public static var defaultVariableA:Array = ["_freeForAll","_tankSpeed","_rotationRate","_shellSpeed","_maxShells","_maxHits", "_enemyNum","_enemyEvasiveness","_startTime","_combatEndDelay"]; // I’m using a static array to store the keycodes. A // keyCode’s index in _keyCodeA is crucial to determine // the tank’s response to that keyCode. The first element // (0-index) is forward, next back, then left, then right. private static var _keyCodeA:Array = [_forwardKeyCode,_backKeyCode,_leftKeyCode,_rightKeyCode]; private static var _playerHits:int; private static var _score:int; private static var _enemiesRemaining:int; public function Data() { // constructor code } // keyCodeA is read-only. I.e., there is no setter for it. public static function get keyCodeA():Array{ return _keyCodeA; } // The 4 controls can be set, but there is no need for a 189
190 Chapter 6 n Developing a Flash Game // getter for them. public static function set forwardKeyCode(n:int):void{ if(keyCodeF(n)){ _keyCodeA[0] = n } } public static function set backKeyCode(n:int):void{ if(keyCodeF(n)){ _keyCodeA[1] = n; } } public static function set leftKeyCode(n:int):void{ if(keyCodeF(n)){ _keyCodeA[2] = n; } } public static function set rightKeyCode(n:int):void{ if(keyCodeF(n)){ _keyCodeA[3] = n; } } private static function keyCodeF(n:int):Boolean{ if(n>=37&&n<=105){ return true; } else { return false; } } public static function set playerHits(n:int):void{ _playerHits = n; } public static function get playerHits():int{ return _playerHits; } public static function set score(n:int):void{ _score = n; } public static function get score():int{ return _score; } public static function set enemiesRemaining(n:int):void{ _enemiesRemaining = n; } public static function get enemiesRemaining():int{
Version _09 return _enemiesRemaining; } public static function set freeForAll(b:Boolean):void{ _freeForAll = b; } public static function get freeForAll():Boolean{ return _freeForAll; } public static function set tankSpeed(n:int):void{ _tankSpeed = n; } public static function get tankSpeed():int{ return _tankSpeed; } public static function set rotationRate(n:int):void{ _rotationRate = n; } public static function get rotationRate():int{ return _rotationRate; } public static function set shellSpeed(n:int):void{ _shellSpeed = n; } public static function get shellSpeed():int{ return _shellSpeed; } public static function set maxShells(n:int):void{ _maxShells = n; } public static function get maxShells():int{ return _maxShells; } public static function set maxHits(n:int):void{ _maxHits = n; } public static function get maxHits():int{ return _maxHits; } public static function set enemyNum(n:int):void{ _enemyNum = n; } public static function get enemyNum():int{ return _enemyNum; } 191
192 Chapter 6 n Developing a Flash Game public static function set enemyEvasiveness(n:int):void{ _enemyEvasiveness = n; } public static function get enemyEvasiveness():int{ return _enemyEvasiveness; } public static function set startTime(n:int):void{ _startTime = n; } public static function get startTime():int{ return _startTime; } public static function set combatEndDelay(n:int):void{ _combatEndDelay = n; } public static function get combatEndDelay():int{ return _combatEndDelay; } // This is the resetAllF() function used to intialize and // reset all the non-keyboard parameters public static function resetAllF():void{ for(var i:int=0;i<defaultValueA.length;i++){ Data[defaultVariableA[i]] = defaultValueA[i]; } } } } This completes the initial coding of the tank combat game. We could do more, but this is enough for now. In the next chapter, I’ll use this game to show you how to test whether a game has a memory or CPU/GPU utilization problem and how to optimize coding to improve performance. And then we’ll try to get this game to run on an iPad, where optimization will certainly be needed.
Chapter 7 Optimizing Game Performance In this chapter, we’ll explore optimizing Flash game performance. By that I mean editing your Flash game so its realized (or actual) frame rate is sufficient to make it appear that your animations are fluid. In general, you should assign the lowest frame rate that yields smooth animation: A frame rate of 24 frames per second (fps) will usually suffice to yield fluid animation. That matches the frame rate used in the motion picture industry. If you’ve seen stuttering animation, you know what you want to avoid. If you’re not sure what stuttering animation looks like, create any SWF with animation (or open the tank combat game), assign a frame rate of less than 10 (for example, 5), and test. That is stuttering animation. Two main components determine game performance, and they aren’t independent of each other: CPU usage and memory usage. I will cover them both in this chapter and discuss how to optimize each. Some optimizations for one will have a negative impact on the other. To understand how that works and why you might judiciously decide to increase memory use to decrease CPU load, for example, read further. If you make a game for a mobile device, you’ll probably need to use at least some of the information in this chapter to achieve acceptable frame rates. If your games are only for the desktop, you can probably achieve acceptable frame rates with some or no familiarity with the information in this chapter. 193
194 Chapter 7 n Optimizing Game Performance Judging and Measuring Performance In an ideal world, part of the Flash test environment would allow you to emulate your target platform and judge how your game is likely to perform on that target platform. Unfortunately, unless your development platform and target platform are the same, currently there is no way to do that. A useful tool on your development platform would reveal the estimated frame rate of your game on your target platform. Again, unfortunately no such tool exists yet. However, you can measure your game’s frame rate using ActionScript, and you can observe your game’s performance on your test computer. The problem is that your measurement and observation will reveal the performance of your game when played on the test platform, not the target platform. And the two (development platform and target platform) may be vastly different. For example, when targeting mobile phones, the target platform will certainly be weak when compared to any platform used for Flash game development (or Flash Pro wouldn’t even install). This causes a major workflow problem. For example, it takes so long to publish a game for a mobile phone, install it, and test it that you will want to do most of your testing on your development computer. But that doesn’t test your game’s performance on the mobile platform. Android has a virtual device emulator (http://developer.android.com/guide/ developing/devices/emulator.html), but that doesn’t help with iOS device emulation, and it doesn’t help reveal the performance that people using different platforms will see when they play your game. In fact, it doesn’t even come close (at this time) to accurately demonstrating the performance of your game on an android device, so it’s not clear to me that the Android emulator is worth using at this time. Until recently, the best you could do was to measure your app’s performance on your development platform and periodically confirm that all was well on the target platform by observing your application’s performance on the target platform. That changed at the end of 2012 when Adobe released Adobe Scout (formerly Adobe Monocle), an application that profiles the performance of Flash content. You can use Scout to profile your app’s memory use, CPU utilization, frame rate, and much more. You can even use remote profiling with it, meaning you can run Scout on your development machine while profiling Flash content running on a mobile device. There has never been anything like that before, and it is a major benefit for Flash developers.
CPU/GPU Usage and Memory Consumption Scout can be downloaded at https://www.adobe.com/products/gaming/tools.edu.html. Check http://www.adobe.com/devnet/flashruntimes/articles/adobe-scout-getting-started. edu.html for information on how to use Scout and what you can do with Scout. There is one thing Scout 1.0 does not yet do, and that is to track which objects are in memory. Scout is very easy to use to detect a memory leak, but it does not indicate which objects are causing the leak. So, having an additional tool that can track which objects are in memory can be very helpful. For that, you can use a memory tracker, which is discussed below. It’s a good idea to remember that mobile devices are such weak computing platforms that you’ll very likely have a performance issue you’ll need to address. If you learn the most important optimization techniques, you may be able to avoid the most common causes of performance problems. And if you learn how to test and compare the various ways to accomplish what you want, you’ll be able to apply the optimum method to your game and give yourself the best chance for satisfactory performance. CPU/GPU Usage and Memory Consumption How your game performs on a desktop or laptop computer depends on the host computer’s processing power, which is determined by the host computer’s CPU. How your game performs on a mobile device not only depends on the device’s CPU, but may also depend on the device’s graphics processor unit (GPU). For the rest of this chapter, I’ll use CPU/GPU to mean the processing power of the host. I intend for that to mean only the CPU when the CPU is the only processor used by your game and to mean the CPU and GPU when your game uses both. If the CPU/GPU is overwhelmed by computing something, even if it’s unrelated to your Flash game, your game will suffer a performance problem. Obviously, you cannot do anything about CPU/GPU usage unrelated to your game, so I’m only going to discuss CPU/GPU usage caused by your game. Also, much of what you read here and elsewhere about Flash, especially as it relates to performance, is dependent on the Flash Player version used or the Adobe Air version used to publish your game. Suffice it to say, there are major differences in performance and memory management when moving from, for example, the iOS player in Flash Pro CS5 to the player used in Flash Pro CS5.5 to the player used in Flash Pro CS6. And, there are major differences among the various versions of the standalone Flash Player and major differences among the browser plug-in Flash Players. 195
196 Chapter 7 n Optimizing Game Performance Everything done by the Flash Player uses CPU/GPU. That means everything displayed in your game and every bit of code that executes in your game requires processing by the CPU/GPU. In addition, if your SWF’s frame rate is greater than zero, even when there’s nothing on-stage and no code executing, your game still uses CPU/ GPU. That is, if you open Flash Pro, save your FLA, and publish your SWF, that game still uses the CPU/GPU when it runs. Even that empty-stage SWF with no code is repeatedly using the CPU/GPU. For example, if your game’s frame rate is 24, then 24 times per second (if the host computer/device can cope with that frame rate), the Flash Player does the following: 1. It checks for any events completed from the previous 1/24 second. 2. Then it executes the listener function code, if there is any, for those events. 3. Then if you have code on a frame that was just entered, that code will execute. 4. Then it dispatches the event. Event.ENTER_FRAME event to all your listeners for that 5. Then the display is rendered. And, that isn’t all the Flash Player has to do. If you create an object (that is, anything) at any time, the Flash Player has to allocate memory for that object, and it has to store that object reference and its value, if any was assigned when it was created. If no memory is currently available, Flash allocates more. But that’s not all the Flash Player has to do. Before it allocates new memory, it checks for objects that can be removed from memory (i.e., garbage collected). If it finds any, it marks them for removal and, on a subsequent clock cycle, it clears that part of memory. And the best (or worst) is yet to come. The marking of objects for removal is itself a dizzying multistep process that involves checking every object that exists and then counting all the references to every existing object. All objects with zero references are marked for removal (sweeping, in Flash parlance) and (eventually) cleared from memory. Other than Steps 3 (game code) and 5 (the graphics displayed), most of us don’t consider the other steps that impact our game’s performance. However, the time required to complete all five steps determines whether your game runs smoothly. If the time required to complete all five steps is greater than 1 fps (frame per second), a
Memory Management frame will remain on-stage longer than 1 fps. Put a few of those frames together back to back, and your game will appear to stutter. I want to convey two main points in this section: n There’s a lot going on in even the most basic Flash game, and all of it uses CPU/ GPU. n Memory management has an impact not just on memory, but also on CPU/ GPU. After seeing what the Flash player does many times per second, it is amazing that, for many gameplayers, there are no noticeable performance problems with most Flash games. However, an increasing number of players who use relatively weak computing devices, such as phones, are having problems with Flash games (and everything else Flash). For example, my tank combat game plays very smoothly on my development computer. But some computers and probably all mobile devices will experience performance problems with that game unless its performance is improved. Memory Management Flash manages memory by allocating it at the start of your game. Then, as mentioned earlier, if Flash needs more memory, it checks for objects that can be removed and frees (i.e., deallocates) some previously allocated and previously used memory. Objects that have no references (in other words, there is no way to address them using ActionScript) can be removed from memory and are marked by Flash for garbage collection (gc). Actually removing an object from memory occurs at some future indeterminate (outside of the test environment) time controlled by Flash. All objects in the display list can be referenced, using getChildAt(), even if you assign the reference name to null. Therefore, no object in the display list can be gc’d. All objects with a non-weak listener can be referenced (via its listener function and the event’s currentTarget property), so all non-weak listeners must be removed before an object can be gc’d. At least, that was the way it used to work. In more recent Flash Players, nulled objects with strong listeners will be gc’d and their listeners removed by Flash. We’ll talk more about this in the upcoming “Weak Listeners versus Strong Listeners” section. But, you cannot depend on everyone having a Flash Player version that does that, and you cannot depend on future Flash Player versions working the same way. So, 197
198 Chapter 7 n Optimizing Game Performance you should remove all listeners when they’re no longer needed as part of good memory management and CPU/GPU management. To determine whether you’re practicing good memory management, you need to measure the memory used by your game. Fortunately, measuring system memory consumption is easy. ActionScript 3.0 has a static System class property totalMemory (or totalMemoryNumber if you’re using a lot of system memory) you can use to measure your game’s memory consumption. Unfortunately, if you have a memory problem, determining what’s causing it isn’t so easy. I will address that in the next section. If, at any time after your game starts, the amount of memory required by your game exceeds the amount of available system memory, your game’s frame rate will drop to zero, and the Flash Player will crash. Obviously, that is a very serious performance problem, and it can occur in two general ways. One way is that upon initial game setup, your game needs more than the available memory. If that applies to your situation, you need to rethink your game’s setup. To be sure, there is no sequence of steps that I can sketch that will help in all situations. It may even be that your game is too sophisticated for its target platform, and nothing short of gutting your game’s essential elements will get it to load and run. That is, the problem may not even be solvable in any meaningful manner that allows the game you originally envisioned to load and run. The second way is that your game’s memory use may increase the longer your game is played. If your game consumes more and more memory the longer it is played, you will eventually see your game’s performance degrade. There must be some limit to memory consumption, or your game will suffer steadily worsening performance and eventually will crash the Flash Player. For both excess memory-usage situations, you can test on your development platform. Because the amount of memory your game uses will be the same on the target platform as on the development platform, you just need to know the amount of memory available to your game on your target platform and then test your game’s memory consumption before deploying to that platform. This second scenario, where your game’s memory use may increase the longer your game is played, is much more common than the first scenario. It is easy to create (and re-create ad infinitum) objects that are never cleared from memory. When that happens, you are certain to see a significant problem eventually. Fortunately, this runaway memory consumption is always solvable. Although finding whether a memory problem exists using one of the System properties is
Memory Tracking, Memory Use, and Performance Testing straightforward, actually pinpointing the problem is not straightforward. Nothing in Flash Pro lets you track what is in the memory used by your Flash game. Among the ways you can test a game’s performance and track memory problems is to use a memory tracker class, which tracks what is in the memory used by your Flash game. One such class follows. It monitors your game’s realized frames per second and its memory use, and it lets you track what is in your game’s memory. You should always test your game before deployment, even if there appears to be no performance problem obvious to you. You should always test to ensure that your game has no memory problem before testing performance on the target platform. This is especially important for games deployed for mobile devices. While browserbased games are unlikely to remain open for weeks, it is typical for mobile users to never close or shut down a game. They are moved to the background and idle when not in use. Mobile games (and apps) are rarely (if ever) closed, so memory problems can accumulate over not just weeks, but months of gameplay. Memory Tracking, Memory Use, and Performance Testing The following code is adapted from Damian Connolly’s code at divillysausages.com. The MT class reports frame rate, memory consumption, and what objects are still in memory. Using it is easy: Import it and initialize it from the document class: import com.kglad.MT; MT.init(this,reportFrequency); where this references your reportFrequency is an integer. document class (that is, the main timeline) and The main timeline reference is used to compute the realized frame rate, and reportFrequency is the frequency in seconds that you want trace output reporting the frame rate and amount of memory consumed by your Flash game. If you don’t want any frame rate/memory reporting, pass 0 (or anything less). You can still use the memory tracker part of this class. To track objects you create in your game, use: MT.track(whatever_object,any_detail); where the first parameter is the object you want to track (to see whether it is ever removed from memory) and the second parameter is anything you want (typically a string that supplies details about what, where, and/or when you started tracking that object). When you want a report of whether your tracked objects still exist in memory, use: MT.report(); 199
200 Chapter 7 n Optimizing Game Performance I’ll show sample code using MT after discussing details of the MT class in the MT class comments. You don’t need to understand the class to use it, but it’s a good idea to check how the Dictionary class is used to store weak references to all the objects passed to MT.track(). Similar to the observer effect in physics, the mere fact that we are measuring the frame rate and/or memory and/or tracking memory changes the frame rate and memory of the game. However, the effect of measurement should be minimal if the trace output is relatively infrequent. In addition, the absolute numbers usually aren’t important. It is the change in frame rate and/or memory use over time that is important, and for that this class works well. This class doesn’t allow more than once per second trace output to help minimize spuriously low frame rate reports caused by frequent use of trace (in other words, frequent trace output slows performance). And, you can always eliminate trace output as a confounder of frame rate determination by using a textfield instead of trace output. The MT class is the only tool you need to check memory usage and pinpoint memory problems, as well as indirectly measure CPU/GPU usage (by checking the actual frame rate of an executing app). com.kglad.MT package com.kglad{ import flash.display.MovieClip; import flash.events.Event; import flash.utils.getTimer; import flash.system.System; import flash.utils.Dictionary; // adapted from @author Damian Connolly // http://divillysausages.com/blog/tracking_memory_leaks_in_as3 public class MT { // Used to calculate the real frame rate private static var startTime:int=getTimer(); // Used to generate trace output intermittently private static var traceN:int=0; // Megabyte constant to convert from bytes to megabytes. private static const MB:int=1024*1024; private static var mc:MovieClip; // d is used to store weak references to objects that you // want to track. It is the essential object used to memory // track in this class. The true parameter used in the // constructor designates that all references are weak.
Memory Tracking, Memory Use, and Performance Testing // That is, the dictionary reference itself won’t prohibit // the object from being gc’d. private static var d:Dictionary = new Dictionary(true); // Used to trigger a report on the tracked objects private static var reportBool:Boolean; // Used to (help) ensure gc takes place private static var gcN:int; // This is the variable that will store the reportFrequency // value that you pass. private static var freq:int; // A constructor is not needed public function MT() { } // traceF() is the listener function for an Enter.ENTER_FRAME // event. If the game is running at its maximum frame rate // (=stage.frameRate), traceF() will be called stage.frameRate // times per second. private static function traceF(e:Event):void { traceN++; // This conditional ensures trace output occurs no more // frequently than every freq seconds. If the game is // running at stage.frameRate, output will occur at // approximately freq seconds. if (traceN%(freq*mc.stage.frameRate)==0) { // This is used to (try and) force gc. gcN = 0; forceGC(); trace("FPS:",int(traceN*1000/(getTimer()startTime)),"||","Memory Use:",int(100*System.totalMemory/MB)/100," MB"); traceN=0; startTime=getTimer(); } } // Called just prior to the above trace() and called just // prior to a memory report. When called just prior to a // memory report, reportBool is assigned true. private static function forceGC():void { // The first call to System.gc() marks items that are // available for gc. The second should sweep them and // the third is for good luck because you can’t count // on anything being predictably gc’d when you think it // should be. mc.addEventListener(Event.ENTER_FRAME,gcF,false,0,true); gcN = 0; 201
202 Chapter 7 n Optimizing Game Performance } private static function gcF(e:Event):void { // System.gc() initiates the gc process during testing // only. It does not work outside the test environment. // So, if you are using this class to test a game that // is installed on a mobile device, System.gc() will not // clear memory of objects ready to be gc’d. System.gc(); gcN++; // 3 System.gc() statements is usually enough to clear // memory of objects that can be cleared from memory. if (gcN>2) { mc.removeEventListener(Event.ENTER_FRAME,gcF,false); // Here’s where reportBool being true triggers // the memory report. if(reportBool){ reportBool = false; reportF(); } } } // Memory report. All objects passed to d in the track() // function, if they still exist, will be displayed by // the trace statement along with the additional information // you passed to track() private static function reportF():void{ trace("** MEMORY REPORT AT:",int(getTimer()/1000)); for(var obj:* in d){ trace(obj,"exists",d[obj]); } } public static function init(_mc:MovieClip,_freq:int=0):void{ mc=_mc; freq=_freq; if(freq>0){ mc.addEventListener(Event.ENTER_FRAME,traceF,false,0, true); } } // This the function you use to pass objects you want tracked. public static function track(obj:*,detail:*=null):void{ d[obj] = detail; }
Optimization Techniques // This is the function that triggers a memory report. public static function report():void{ reportBool = true; forceGC(); } } } Before I even applied this to the tank combat game, I took some steps to help ensure that I wouldn’t have runaway memory consumption. In the previous chapter, I started using an Event.REMOVED_FROM_STAGE listener for all my DisplayObject classes and doing cleanup in the listener function. That cleanup consisted of removing the listeners I had previously added and making sure objects created in the class were removed from the display list and nulled. If you do that so there are no references to an object, it is not in the display, and it has no (strong) listeners, then it should be eligible for garbage collection (gc). Unfortunately, there are exceptions to that rule. While testing the tank combat game, I found an unexpected issue with sounds and an unexpected issue with timers. There are probably more exceptions. Optimization Techniques Unfortunately, I know of no completely satisfactory way to organize the information on optimization techniques. In what follows, I’ll discuss memory management, with subtopics listed in alphabetical order. Then I’ll discuss CPU/GPU management, with subtopics listed in alphabetical order. That may seem logical, but there are at least two problems with that organization. n I don’t believe it’s the most helpful way to organize this information. n Memory management affects CPU/GPU usage, so everything in the Memory Management section could also be listed in the CPU/GPU section. Anyway, first I’m going to also list the information in two other ways—from easiest to hardest to implement, and from greatest to least benefit. Both of these listings are subjective and depend on developer experience and capabilities, as well as the test situation and test environment. I very much doubt that there could be a consensus on ordering of these lists. Nevertheless, I still think they’re worthwhile. 203
204 Chapter 7 n Optimizing Game Performance Easiest to Hardest to Implement 1. Do not use Filters. 2. Always use reverse 3. Explicitly stop for Timers loops and avoid do loops and while loops. to ready them for gc. 4. Use weak event listeners and remove listeners. 5. Strictly type variables whenever possible. 6. Explicitly disable mouse interactivity when it isn’t needed. 7. Replace 8. Stop dispatchEvents Sounds to enable 9. Use the most basic 10. Always use devices). 11. Reuse 12. with callback functions whenever possible. Sounds DisplayObject cacheAsBitmap Objects and and SoundChannels to be gc’d. needed. cacheAsBitmapMatrix with air apps (that is, mobile whenever possible. loops: Use different listeners and different listener functions applied to as few DisplayObjects as possible. Event.ENTER_FRAME 13. Pool Objects instead of creating and gc’ing Objects. 14. Use partial blitting. 15. Use stage blitting. 16. Use Stage3D. Greatest to Least Benefit 1. Use stage blitting (if there is enough system memory). 2. Use Stage3D. 3. Use partial blitting. 4. Use cacheAsBitmap and cacheAsBitmapMatrix with mobile devices. 5. Explicitly disable mouse interactivity when it isn’t needed. 6. Do not use Filters. 7. Use the most basic DisplayObject needed.
Optimization Techniques 8. Reuse 9. Objects whenever possible. loops: Use different listeners and different listener functions applied to as few DisplayObjects as possible. Event.ENTER_FRAME 10. Use reverse 11. Pool for Objects loops and avoid do loops and instead of creating and gc’ing while loops. Objects. 12. Strictly type variables whenever possible. 13. Use weak event listeners and remove listeners. 14. Replace dispatchEvents 15. Explicitly stop 16. Stop Sounds Timers to enable with callback functions whenever possible. to ready them for gc. Sounds and SoundChannels to be gc’d. Memory Management Events, Filters, DisplayObjects, Object pooling, Sounds, Timers and Listeners. Callback Function versus dispatchEvent There is an increase in memory use when dispatching events because the event must be created, memory must be allocated, and so on. That makes sense because events are objects and therefore require memory. A simple test shows that different event types use different amounts of memory. var e:Event = new Event("testE"); var me:MouseEvent = new MouseEvent("testE"); var te:TouchEvent = new TouchEvent("testE") var ae:AccelerometerEvent = new AccelerometerEvent("testE") var de:DataEvent = new DataEvent("testE"); var ge:GestureEvent = new GestureEvent("testE"); var ee:ErrorEvent = new ErrorEvent("testE"); var fe:FocusEvent = new FocusEvent("testE"); var fse:FullScreenEvent = new FullScreenEvent("testE"); var tx:TextEvent = new TextEvent("testE"); // getSize returns the argument’s size in bytes trace(getSize(e),getSize(me),getSize(te),getSize(ae),getSize(de),getSize(ge), getSize(ee),getSize(fe),getSize(fse),getSize(tx)) // output in bytes: 40 104 128 72 48 72 52 56 48 48 205
206 Chapter 7 n Optimizing Game Performance Using callback functions used less memory and ran more efficiently than using events. (See the test files in /support files/Chapter 07/callback_v_dispatchEvent.) Filters Using a dynamic filter increases memory use. According to Adobe documents (http:// help.adobe.com/en_US/as3/mobile/flashplatform_optimizing_content.pdf), using a filter doubles memory use. However, while I see an increase in memory use when using filters, I don’t see anything close to doubling of memory use when testing with Flash Pro CS6. When testing, I saw a 2-MB increase in memory use (from 12 MB to 14 MB) for 2,000 objects with filters, and a 0.2-MB increase in memory use (from 4.6 MB to 4.8 MB) for 200 objects. (See the test files in /support files/ Chapter 07/filters.) Objects DisplayObjects, Object pooling, and Object reuse. Display Objects The Shape, Sprite, and MovieClip objects have different memory requirements. A bytes. Shape requires 236 bytes, a Sprite 412 bytes, and a MovieClip 448 If you’re using many thousands of display objects, you may be able to save substantial memory by using a Shape if no interactivity is needed and a Sprite if no timeline is needed. But if you had thousands of display objects, you would need to use optimization beyond using a different display object type. Under normal circumstances there’s not much to be gained by changing from one display type to another. For example, in the tank combat game, I could’ve had the Tank class extend the Shape instead of the MovieClip class. That would save a maximum of 21 tanks x 212 bytes per tank/1024 bytes per KB, which is less than 4.4 KB. That’s not enough to make me seriously consider changing to use the Shape object type in the tank combat game. Object Pooling The concept of object pooling is simple. At the start of your game, you create all the objects you’ll ever need during the entire time your game is played and pool them in an array. Whenever an object is needed, you retrieve it from the array. Whenever an object is no longer needed, you return it to the array. This releases the Flash Player from gc work. It no longer has to mark and sweep and allocate new memory. The only downsides are that your game will use the maximum
Optimization Techniques amount of memory at the game’s start, and there is a little additional coding. Neither of these is much of a drawback. Because the main point of using object pooling is efficiency, you’ll typically use a vector instead of an array. Using a vector may be twice as fast as using an array, but unless you’re doing many hundreds of thousands of operations, you won’t likely notice a difference because both are fast when limited to thousands of operations. (See the array_v_vector folder.) Here’s code for a com.kglad.IntroView_Pool class. package com.kglad{ public class IntroView_Pool { private static var pool:Vector.<IntroView>; public static function init(poolSize:int):void { pool = new Vector.<IntroView>(poolSize); for(var i:int=0;i<pool.length;i++){ pool[i] = new IntroView(); } } public static function retrieveF():IntroView { if (pool.length>0) { return pool.pop(); } else { // this branch should not execute. return new IntroView(); } } public static function returnF(view:IntroView):void { pool.push(view); } } } The IntroView_Pool class will use a poolSize of 1 because that is the most IntroView instances we would need at any one time. But this still will be helpful and will save the repeated creation and gc’ing of IntroView instances every time a game is replayed. Object pooling should be even more helpful for EnemyTank instances where we could create the maximum number (20) in an EnemyTank_Pool class and avoid repeated creation and gc’ing of 1 to 20 EnemyTank instances with each game replay. When using object pooling, make sure there’s not much more in the object constructor than an Event.ADDED_TO_STAGE event. In particular, you don’t want any Event.ENTER_FRAME or Timer listeners added to objects you were going to pool. And, 207
208 Chapter 7 n Optimizing Game Performance you don’t want to null those objects and ready them for gc when you’re finished with them. You should return them to the pool using something like returnF() in IntroView_Pool. Here’s sample code to use IntroView_Pool. To initialize the pool, use: IntroView_Pool.init(1); in the Main constructor. Then in addIntroViewF(), instead of: introView = new IntroView(); you would use: introView = IntroView_Pool.retrieveF(); and instead of: introView = null; in removeIntroViewF(), you would use: IntroView_Pool.returnF(introView); You still want to remove the introView listener in removeIntroViewF(). Reuse Objects Whenever you create objects in a loop, see whether you can create one object outside the loop and reuse it repeatedly inside the loop. You can’t always do that, but there are many places where you can. For example, in the tank combat game, I was able to use one shellShot sound and one destroyTank sound repeatedly. And later, in the “Blitting” section, I’ll reuse a number of objects. Sounds The issue with sounds is relatively minor. When a sound is playing, it cannot be gc’d (at least not when using Flash Pro CS6 to test). When the sound completes playing or a SoundChannel instance is used to stop() the sound, the sound can be gc’d. (See below or the test files in /support files/Chapter 07/gc_sound_test.) import flash.events.TimerEvent; import flash.media.SoundChannel; import com.kglad.MT; MT.init(this,0); // Sound test showing sound must not be playing in order to be gc’d. // If soundchannel used, must explictly apply stop() to it for sound // and soundchannel to be eligible for gc.
Optimization Techniques // If no soundchannel used, sound is only eligible for gc after sound // completes play. var sc:SoundChannel; for(var i:int=0;i<6;i++){ // ShotSound is 7.3 seconds in duration. var s:Sound = new ShotSound(); // Comment out one of the two following lines to see the // difference between using a SoundChannel instance to stop // play and not using one //s.play(); sc = s.play(); MT.track(s,"sound_"+i); if(sc){ MT.track(sc,"soundchannel_"+i); } } // If gc mark/sweep after sound’s duration, sounds and soundchannels // eligible for gc. Otherwise, if gc mark/sweep before sound’s duration, // the sound is only eligible for gc if a soundchannel is used AND if stop() // is applied to the soundchannel. In that situation, not only the sound // but the soundchannel is eligible for gc. // Comment out one of the two lines below to see the difference // between how Flash handles sounds that have and have not completed play. var delay:int = 7000; //var delay:int = 8000; var t:Timer = new Timer(delay,1); t.addEventListener(TimerEvent.TIMER,stopF); t.start(); function stopF(e:TimerEvent):void{ if(sc){ sc.stop(); sc=null; } if(s){ s=null; } MT.report(); } This isn’t too troubling. It explains why we were sometimes seeing more than two Sound instances when we were testing the tank combat game, but otherwise it’s not significant as long as sounds end shortly after they are no longer needed. Sounds otherwise readied for gc will eventually be gc’d after they complete playing if the Flash Player needs memory. 209
210 Chapter 7 n Optimizing Game Performance That shouldn’t cause problems unless you have a lot of dead space at the end of your sounds. Then you should probably use a SoundChannel to stop your sound when it’s no longer needed or, even better, use a sound editor to remove that dead space. Timers The issue with Timers is more serious. If a Timer hasn’t stopped (because its currentCount is less than its repeatCount and because stop() hasn’t been applied to it), it won’t be gc’d even if you remove its listener and null all references. Its listener function won’t be called once you remove the listener, but the object itself will still consume memory. In the tank combat game, we had a directionChange Timer that had no end (repeatCount=0). It repeatedly called a function that changed the EnemyTank’s direction. Actually, each EnemyTank had its own Timer, so when several enemies were used, several of these Timers were created. And, they were never gc’d because we weren’t aware they needed to be stopped. We wouldn’t have found that issue if we hadn’t used MT and seen that numerous timers were being created and never gc’d round after round of play. A timer uses 72 bytes of memory, so you wouldn’t expect a noticeable problem in a desktop/browser Flash game. Even after 100 rounds with 20 Timers being created and not gc’d, you would only have 72 × 100 × 20/1024 < 141 KB additional memory consumption. But, on a mobile device, for example, a game could be opened and played, idled, opened and played, and so on without ever restarting, and that could cause a noticeable problem. Here is the code I used to confirm the issue. (See the test files in /support files/ Chapter 07/gc_timer_test.) import flash.events.TimerEvent; import com.kglad.MT; MT.init(this,0); // Timer test showing timer.stop() needed unless timer // currentCount == repeatCount // If timer.stop() used, the listener doesn’t even need to be removed // for the timer to be gc’d. var t:Timer = new Timer(100,0); // Even with a weak listener and even when that listener is removed, // it won’t be gc’d unless it’s explicitly stopped. t.addEventListener(TimerEvent.TIMER,traceF,false,0,true); t.start(); MT.track(t,"timer");
Optimization Techniques function traceF(e:TimerEvent):void{ //t.stop(); // <– needed. Uncomment this and the timer is removed // even if you comment out the line below that removes the listener. t.removeEventListener(TimerEvent.TIMER,traceF,false); t=null; // The code below is used to check if the timer has been removed // even at a later time. var t1:Timer = new Timer(1000,10); t1.addEventListener(TimerEvent.TIMER,reportF); t1.start(); } function reportF(e:TimerEvent):void{ MT.report(); } Weak Listeners versus Strong Listeners Another unexpected result of testing with MT is that it makes no difference whether you use weak or strong listeners. They are both treated like weak listeners and do not prevent gc’ing of the event dispatcher. (See below or the test files in /support files/ Chapter 07/strong_v_weak_listeners.) import flash.display.MovieClip; import flash.events.Event; import com.kglad.MT; MT.init(this,0); for(var i:int=0;i<12;i++){ var mc:MovieClip = new MovieClip(); mc.name = i.toString(); // even with strong listeners, these movieclips are all gc’d // there are no references to the first 11 by the end of the loop // and the refrence to the 12th mc is nulled below. mc.addEventListener(Event.ENTER_FRAME,ff); // If you uncomment addChild(mc), none of the mc will be gc’d. //addChild(mc); MT.track(mc,"exists "+i); } function ff(e:Event):void{ trace(e.currentTarget.name); } // If you comment-out the next line, the last created mc will continue // to exist and its listener will continue to call ff(). mc=null; MT.report(); 211
212 Chapter 7 n Optimizing Game Performance Managing CPU/GPU Usage The only way I know to currently measure CPU/GPU usage directly is to use an operating system tool. Windows has the Windows Task Manager (Performance tab), and Mac has the Activity Monitor. Both allow you to see CPU usage, but neither is very useful for testing Flash game performance. So, you are left measuring this indirectly by checking your game’s actual frame rate. Among other things, the MT class does that. Arithmetic Operations You can gain efficiency by using bitwise operators and shortcutting some of the Math class methods. But unless you’re performing many millions of operations, none of the bitwise methods are worth the trouble, in my opinion. Bitwise Operators You can easily apply bitwise operators to any situation where you multiply or divide by a power of two. For example, instead of halving (frequently used for centering objects with a corner registration point), you can use a bitwise shift: x/2 = x>>1 This makes almost no difference at all but is so widely repeated as a benefit that it seems to be an accepted fact. Maybe it was (and is) a greater benefit in some Flash Player versions, but although I’ve seen bitwise shifting touted on numerous websites, none has cited any test data to support the claim. Here are the results of my testing in /support files/Chapter 07/bitwise_arithmetic/ halving.fla. Division /* No significant difference using bitwise operation vs division. This results below were the most favorable to bitwise division. Some tests showed faster execution using multiplication and/or division. */ var xx:Number; var n:int = 10000000; var i:int; var startTime:int; ///* startTime = getTimer()
Managing CPU/GPU Usage // division duration 445 for(i=0;i<n;i++){ xx = i/2; } trace("division duration",getTimer()-startTime); //*/ ///* startTime = getTimer() // multiplication duration 440 for(i=0;i<n;i++){ xx = i*.5; } trace("multiplication duration",getTimer()-startTime); //*/ ///* startTime = getTimer() // bitwise duration 435 for(i=0;i<n;i++){ xx = i>>1; } trace("bitwise duration",getTimer()-startTime); //*/ It makes even less sense to use bitwise arithmetic for doubling. (See/support files/ Chapter 07/bitwise_arithmetic/halving.fla.) Multiplication // No significant benefit using bitwise operation vs multiplication var xx:int; var n:int = 10000000; var i:int; var startTime:int; ///* startTime = getTimer(); // arithmetic duration 434 for(i=0;i<n;i++){ xx = i*2; } trace("arithmetic duration",getTimer()-startTime); //*/ ///* startTime = getTimer(); // bitwise duration 431 for(i=0;i<n;i++){ 213
214 Chapter 7 n Optimizing Game Performance xx = i<<1; } trace("bitwise duration",getTimer()-startTime); //*/ Likewise, using bitwise shifting instead of multiplying and dividing by other powers of two provides negligible benefit. There is a small benefit to using bitwise arithmetic in place of the modulo operator. (See /support files/Chapter 07/bitwise_arithmetic/modulo.fla.) Modulo // small benefit using bitwise operation vs modulo var p:int = 37; var i:int; var n:int = 10000000; var startTime:int; /* startTime = getTimer() // modulo duration 565 for(i=38;i<n;i++){ if(i%p==0){ } } trace("modulo duration",getTimer()-startTime); */ /* startTime = getTimer() // bitwise duration 451 var p_1:int = p-1; for(i=38;i<n;i++){ if((i&(p_1))==0){ } } trace("bitwise duration",getTimer()-startTime); */ ///* // bitwise duration 452 for(i=38;i<n;i++){ if((i&(p-1))==0){ } } trace("bitwise duration",getTimer()-startTime); //*/
Managing CPU/GPU Usage Math Class Shortcuts These methods actually do provide a benefit, and that benefit can be significant even without needing millions of operations to realize. They are listed in alphabetical order starting with Math.abs. (See /support files/Chapter 07/ math_class/Math.abs_v_conditional.fla, /support files/Chapter 07/math_class/Math .floor_v_int.fla, and so on.) Math.abs Using: x = (y<0) ? -y: y; instead of: x = Math.abs(y) is about twice as fast. Unless you’re using millions of Math.abs operations, you shouldn’t expect a noticeable benefit from using the inline code. In addition, the inline code is cumbersome. var xx:int; var n:int = 10000000; var n2:int = n/2; var i:int; var startTime:int = getTimer(); //// Math.abs duration: 1016 for(i=0;i<n;i++){ xx = Math.abs(n2-i); } //*/ trace("Math.abs duration:",getTimer()-startTime); ///* // conditional duration: 445 startTime = getTimer(); for(i=0;i<n;i++){ xx = (n2-i<0) ? i-n2 : n2-i; } //*/ trace("conditional duration:",getTimer()-startTime); Math.ceil and Math.floor Using: x = int(y); instead of: 215
216 Chapter 7 n Optimizing Game Performance x = Math.floor(y); // y>=0 x = Math.ceil(y); // y<=0 is about twice as fast. Unless you’re using millions of Math.floor operations (with nonnegative numbers), you shouldn’t expect a noticeable benefit. var i:int; var n:int = 10000000; var xx:int; var startTime:int = getTimer(); ///* // Math.floor duration: 1105 for(i=0;i<n;i++){ xx = Math.floor(i/n); } trace("Math.floor duration:",getTimer()-startTime); //*/ ///* // int duration: 479 startTime = getTimer(); for(i=0;i<n;i++){ xx = int(i/n); } //*/ trace("int duration:",getTimer()-startTime); Math.max Using: x = (i>j) ? i : j; instead of: x = Math.max(i,j); is about twice as fast. This shortcut is also cumbersome but has the greatest benefit (along with Math.min) of all those listed in this section. Notice the difference in time required to execute the code blocks and how few iterations are needed to demonstrate that difference. var xx:int; var n:int = 1000; var i:int; var j:int; var startTime:int; ///*
Managing CPU/GPU Usage // Math.max duration: 109 startTime = getTimer(); for(i=n-1;i>=0;i– –){ for(j=n-1;j>-0;j– –){ xx = Math.max(i,j); } } trace("Math.max duration:",getTimer()-startTime); //*/ ///* // conditional duration 43 startTime = getTimer(); for(i=n-1;i>=0;i– –){ for(j=n-1;j>-0;j– –){ xx = (i>j) ? i : j; } } //*/ trace("conditional duration",getTimer()-startTime); Math.min Using: x = (i<j) ? i : j; instead of: x = Math.min(i,j); is about twice as fast. This shortcut is also cumbersome but has the greatest benefit (along with Math.max) of all those listed in this section. Notice the difference in time required to execute the code blocks and how few iterations are needed to demonstrate that difference. var xx:int; var n:int = 1000; var i:int; var j:int; var startTime:int; ///* // Duration Math.min 121 startTime = getTimer(); for(i=0;i<n;i++){ for(j=0;j<n;j++){ xx = Math.min(i,j); } 217
218 Chapter 7 n Optimizing Game Performance } //*/ trace("Duration Math.min",getTimer()-startTime); ///* // Duration conditional 43 startTime = getTimer(); for(i=0;i<n;i++){ for(j=0;j<n;j++){ xx = (i<j) ? i : j; } } //*/ trace("Duration conditional",getTimer()-startTime); Math.pow It is two to three times faster to explicitly multiply a number variable than it is to use Math.pow. That benefit even extends a little beyond integer exponents because Math. sqrt(i) is about twice as fast as Math.pow(i,.5). var i:int; var n:int = 10000000; var xx:int; var startTime:int; ///* // exp .5: 2020, exp 2: 1533, exp 3: 1617, exp 4: 1427, exp 5: 1381, // exp 10: 1391 startTime = getTimer(); for(i=0;i<n;i++){ xx = Math.pow(i,.5); } trace("Duration Math.pow",getTimer()-startTime); //*/ ///* // exp .5: 1064, exp 2: 427, exp 3: 778, exp 4: 557, exp 5: 501, // exp 10: 586 startTime = getTimer(); for(i=0;i<n;i++){ xx = Math.sqrt(i); } trace("Duration iteration",getTimer()-startTime); //*/ Bitmaps cacheAsBitmap, cacheAsBitmapMatrix, blitting and partial blitting.
Managing CPU/GPU Usage cacheAsBitmap and cacheAsBitmapMatrix One of the most basic methods to decrease CPU usage is to minimize the work of the Flash Player and CPU when the display is rendered by using bitmaps or DisplayObjects that have their cacheAsBitmap property enabled. You should see a major performance boost when applying cacheAsBitmap appropriately. You should also see memory utilization increase because a bitmap of that vector DisplayObject is created in memory and used to render the display. Enabling the cacheAsBitmap property will significantly improve performance as long as the DisplayObject doesn’t undergo any changes that require an update to the bitmap. Essentially, that means your DisplayObject doesn’t change appearance in any way other than changing its location on the stage. That is, its x and/or y can change, but anything else will require an update to the bitmap. If there are frequent bitmap updates, performance will decrease rather than improve. How frequently you can update a cached bitmap and still see a performance benefit depends on several factors, the most important of which is, not surprisingly, how frequently you update the bitmap. In any case, use MT to test your specific situation both with and without cacheAsBitmap enabled for DisplayObjects that require bitmap updates. (It’s a no-brainer when it comes to using cacheAsBitmap for DisplayObjects that require no bitmap updates: Use it!) So, if your DisplayObject is a MovieClip with a multiframe timeline with changing shapes on the different frames, or if you change the scale, skew, alpha, or rotation of your MovieClip too frequently, enabling the cacheAsBitmap property isn’t helpful because all those things require an update to the cached bitmap—i.e., the data describing the bitmap needs to be completely re-written to memory. If you have a MovieClip ActionScript, use: mc and you want to enable its cacheAsBitmap property using mc.cacheAsBitmap = true; You can also use the Properties panel to enable the MovieClips created in the Flash Pro IDE. cacheAsBitmap property of There is one important exception that always makes enabling cacheAsBitmap beneficial even when changing the scale, skew, alpha, and/or rotation of a DisplayObject (but not changing frames of a MovieClip). That exception occurs when publishing games for mobile devices. Specifically, when publishing for mobile devices, you can enable cacheAsBitmap, assign the cacheAsBitmapMatrix property of your DisplayObjects, and realize a substantial performance boost (as long as you aren’t changing MovieClip frames): 219
220 Chapter 7 n Optimizing Game Performance mc.cacheAsBitmap = true; mc.cacheAsBitmapMatrix = new Matrix(); With both of those properties assigned, you can apply any two-dimensional transform and/or alpha change to mc without causing an update (which would negatively impact performance) to the cached bitmap. You don’t have to use the default identity matrix, but there are only a few reasons to use something other than the default matrix. One would be to save a scaled-down bitmap of mc to save memory (using a matrix with a=d properties < 1), and another would be to save a scaled-up bitmap to minimize scaling and aliasing artifacts that may be caused by changes to mc’s scale, skew, or rotation properties. Blitting Stage blitting and partial blitting. Stage Blitting Stage blitting, short for bit-block transferring, is the use of bitmaps to render the final display. That is, instead of adding objects like MovieClips, Sprites, and so on to the display list, pixels are drawn to a stage-sized bitmap, and the bitmap is added to the stage. To convey animation, the bitmap’s pixels are updated in a loop, typically an Event.ENTER_FRAME loop using the BitmapData class’s copyPixel() method applied to the stage-sized bitmap’s bitmapData property using other bitmapData objects created outside the animation loop. This technique is more complicated than adding objects directly to the display list, but it’s much more efficient, often making the difference between unacceptable and excellent frame rates for a game. To be sure, there is absolutely no reason to use this unless you need the increased frame rate. Here’s an example of 10,000 squares moving and rotating across the stage using MovieClips. This code is in /support files/Chapter 07/blit_test/blit_test_mc.fla. // fps test comparing movieclips vs blitting. // On my computer this ran at about 15 frames per second and // consumed 29.5mb import flash.events.Event; import flash.display.MovieClip; import com.kglad.MT; MT.init(this,3); // Then number of square MovieClips var num:int=10000; // The length of each MovieClip square var rectSide:int = 2;
Managing CPU/GPU Usage // The rate of rotation of each MovieClip var rotationRate:int = 1; var mc:MovieClip; var mcA:Array=[]; movieclipF(); function movieclipF():void { // Create the MovieClips, assign a speed of 1 or 2, an angle of 0 to 359, // an initial x and y that places the MovieClip on-stage for (var i:int=0; i<num; i++) { mc = new MovieClip(); with (mc.graphics) { beginFill(0xaa0000); // Draw square with offset registation point drawRect(-rectSide/2,-rectSide/2,rectSide,rectSide); endFill(); } mc.speed = 1+int(2*Math.random()); mc.angle = int(360*Math.random()); mc.x = int(Math.random()*(stage.stageWidth-mc.width/2)); mc.y = int(Math.random()*(stage.stageHeight-mc.height/2)); addChild(mc); // Enabling the cacheAsBitmap property is expected to decrease // performance because these MovieClips are going to be rotated // in the animation loop. But I was surprised to see how much // performance suffered when enabling. mc.mouseEnabled=false; mc.mouseChildren=false; mcA.push(mc); } this.addEventListener(Event.ENTER_FRAME,animateMovieClipsF); } function animateMovieClipsF(e:Event):void { for (var i:int=0; i<mcA.length; i++) { mc=MovieClip(mcA[i]); mc.rotation+=rotationRate; // Each MovieClip moves along its angle mc.x+=mc.speed*Math.cos(mc.angle*Math.PI/180); mc.y+=mc.speed*Math.sin(mc.angle*Math.PI/180); // If a stage boundary is encountered, the MovieClip ricochets if (mc.x>stage.stageWidth-mc.width/2) { mc.angle=180-mc.angle; } else if (mc.x<mc.width/2) { mc.angle=180-mc.angle; } else if (mc.y>stage.stageHeight-mc.height/2) { 221
222 Chapter 7 n Optimizing Game Performance mc.angle*=-1; } else if (mc.y<mc.height/2) { mc.angle*=-1; } } } We get about 15 fps (frames per second) with that code, which is unacceptable. But there are a few basic things we can do to improve performance before embarking on more difficult-to-institute considerations, such as blitting. Namely, we can reverse those for loops to gain a little performance boost (see the upcoming “Loops” section), and, more importantly, we can use some constants instead of recalculating the same values repeatedly. Here’s the same code using constants instead of calculations in several places and using reverse for loops. This code is in /support files/Chapter 07/blit_test/blit_ test_mc_basic_optimizations.fla. // fps test comparing movieclips vs blitting. // With these constants the coding is actually easier to read and follow. // 21fps,48mb with mcA an array // 22fps,48mb with mcA a vector // 23fps, 28mb with mcA a vector and multiplying by 1 in animateMovieClipsF(). import flash.events.Event; import flash.display.MovieClip; import com.kglad.MT; import flash.geom.Point; // fps test comparing movieclips vs blitting. MT.init(this,3); var num:int=10000; var rectSide:int = 2; var rotationRate:int = 1; var i:int; // constants for stage width and height. It is faster to repeatedly use stageW // than to repeatedly use stage.stageWidth var stageW:int = stage.stageWidth; var stageH:int = stage.stageHeight; // constant close enough for mc.width/2 and mc.height/2 var rectHalf:Number = rectSide/2; // Temporary variable used to calculate vectorX and vectorY below, where // vectorX is the magnitude of the x increment per loop and vectorY is // the magnitude of the y increment per loop. var initialDirection:Number; // Temporary variable used to calculate vectorX and vectorY below
Managing CPU/GPU Usage var speed:Number; var mc:MovieClip; //var mcA:Array=[]; var mcA:Vector.<MovieClip> = new Vector.<MovieClip>(num); movieclipF(); function movieclipF():void { // It is faster to loop from end to start than it is to loop from // start to end for(i=num-1;i>=0;i– –){ mc = new MovieClip(); with (mc.graphics) { beginFill(0xaa0000); drawRect(-rectHalf,-rectHalf,rectSide,rectSide); endFill(); } speed = 1+2*Math.random(); initialDirection = 360*Math.random()*Math.PI/180; mc.x = int(rectHalf+Math.random()*(stageW-rectSide)); mc.y = int(rectHalf+Math.random()*(stageH-rectSide)); mc.vectorX = speed*Math.cos(initialDirection); mc.vectorY = speed*Math.sin(initialDirection); addChild(mc); mc.mouseEnabled=false; mc.mouseChildren=false; mcA[i] = mc; } this.addEventListener(Event.ENTER_FRAME,animateMovieClipsF); } function animateMovieClipsF(e:Event):void { for(i=num-1;i>=0;i– –){ mc=MovieClip(mcA[i]); mc.rotation+=rotationRate; // This is quirky. If I do not perform some aritmetic operation(s) // (like multiplying by 1), memory use increases significantly // because of each of these two lines of code. mc.x += 1*mc.vectorX; mc.y += 1*mc.vectorY; if (mc.x>stageW-rectHalf || mc.x<rectHalf) { mc.vectorX *= -1; } if (mc.y>stageH-rectHalf || mc.y<rectHalf) { mc.vectorY *= -1; } } } 223
224 Chapter 7 n Optimizing Game Performance This provides a significant (~40 percent) speed boost to just about acceptable (to our eyes) frame rates, ~21fps. Now let’s see what blitting can do. Using stage blitting to encode the same display is more complex, as previously mentioned. The steps involve: 1. Initializing the stage display bitmap assets (Bitmap instance, BitmapData instance, and Rectangle instance) onto which all the displayed pixels will be copied during each Event.ENTER_FRAME event loop. 2. Populating a data array with all the data used to update the display. (This isn’t always necessary.) 3. Populating an array of BitmapData objects. If you had a MovieClip timeline with animation on it, this is where you would store a BitmapData object of each MovieClip frame (for example, using a sprite sheet; see www.adobe.com/devnet/ flash/articles/using-sprite-sheet-generator.html). In the test file I created a BitmapData instance for each angle the rectangles can be rotated using ActionScript. 4. Creating an Event.ENTER_FRAME event loop. 5. Updating the data in the Event.ENTER_FRAME loop and copying the appropriate pixels from the array created in Step 3 to the appropriate location (determined using the data array from Step 2) of the BitmapData instance created in Step 1. Here’s the same example with squares moving and rotating across the stage, using stage blitting and the optimizations used in the second MovieClip example. (See the test file in /support files/Chapter 07/blit_test_blitting_basic_optimizations.) // 55fps,6mb with arrays // 56fps,5.5mb with vectors, no multiplying by 1 // 58+fps, 5.5mb with vectors, multiplying by 1. import flash.events.Event; import flash.display.MovieClip; import com.kglad.MT; import flash.display.Stage; // fps test comparing movieclips vs blitting. // The next 4 lines define same variables as in the MovieClip example MT.init(this,3); var num:int=10000; var i:int; var stageW:int = stage.stageWidth; var stageH:int = stage.stageHeight; var rectSide:int = 2; var rectHalf:Number = rectSide/2;
Managing CPU/GPU Usage var rotationRate:int = 1; // The background color that will be applied to the display bitmap. var bgColor:uint = 0x000000; // Display rectangle used to "erase" the previously painted pixels. var displayBG_Rect:Rectangle = new Rectangle(0,0,stageW,stageH); // The display’s bitmapData object var displayBMPD:BitmapData = new BitmapData(stageW,stageH,false,bgColor); // The display bitmap onto which all pixels will be copied and the only // thing added to the stage and viewed by the Flash user. var displayBMP:Bitmap = new Bitmap(displayBMPD); // Initialize objects needed for steps 2 and 3. var dataA:Array=[]; var datumA:Array; var loopN:int var bmpdA:Array = []; var bmpdAL:int; var diagL:Number = Math.sqrt(2*rectSide*rectSide); var diagLHalf:Number = diagL/2; var bmpRect:Rectangle=new Rectangle(0,0,diagL,diagL); var pt:Point = new Point(); var speed:Number; var initialDirection:Number; // Populate dataA (step 2). initDataF(); // Populate bmpdA (step 3). bitmapDataF(); function initDataF():void { // Instead of creating num MovieClips with initial positions, // speeds and angles, I’m creating num arrays (datumA), which // will contain initial position (a point), and vectorX and // vectorY (direction of movement). There is no object that // corresponds to the datumA data. These are abstract quantities // that will be applied to the bitmapData objects bmpdA (created // in bitmapF). for(i=num-1;i>=0;i–){ datumA = []; // datumA’s first element will be the initial position, // which will be updated in the Event.ENTER_FRAME loop datumA.push( new Point( int(Math.random()*(stage.stageWidth-diagL)), int(Math.random()*(stage.stageHeight-diagL)) ) ); // speed speed = 1+2*Math.random(); 225
226 Chapter 7 n Optimizing Game Performance // initialDirection used with speed to define vectorX // and vectoryY. Thereafter, they are not needed. initialDirection = 2*Math.PI*Math.random(); datumA.push(speed*Math.cos(initialDirection)); datumA.push(speed*Math.sin(initialDirection)); // datumA = [point,vectorX,vectorY] // Each datumA is added to dataA. DataA serves the // same purpose as mcA in the MovieClip example. dataA[i] = datumA; } } // This is where step 3 is done function bitmapDataF():void{ // I need a temporary display object that I can use to // populate bmpA. mc will be gc’d because it’s local // to bitmapF() var mc:MovieClip = new MovieClip(); with(mc.graphics){ beginFill(0xaa0000); drawRect(-rectHalf,-rectHalf,rectSide,rectSide); endFill(); } // I need a temporary matrix so I can apply a rotation to mc // when I use the draw() method to transfer mc’s pixels to a // bitmapData instance. The draw() method creates a bitmapData // instance of the untransformed object. That is, the object as // it appears in your library. Any change you want to be seen in // the bitmapData object has to be applied via one or more of // the draw() parameters, which include a transform matrix, color // transform, blend mode, clip rectangle and smoothing. var mat:Matrix = new Matrix(); for (i=0; i<360; i+=rotationRate) { // Instantiate a bitmapData instance large enough to // contain the rotated square var bmpd:BitmapData = new BitmapData(diagL,diagL,true,0x00ffff00); // Unapply the previous changes to mat so it can be reused. mat.identity(); // Apply a rotation to mat mat.rotate(i*Math.PI/180); // Apply a translation to mat so mc is positioned in the // center of bmpd mat.tx+=diagLHalf; mat.ty+=diagLHalf; // Apply the draw() method using mat to make the square
Managing CPU/GPU Usage // appear rotated. bmpd.draw(mc,mat); // Add this bitmapData instance to bmdA so it can be used // in our Event.ENTER_FRAME loop. bmpdA[i] = bmpd; } bmpdAL = bmpdA.length addChild(displayBMP); // loopN used to count number of Event.ENTER_FRAME loops and // choose which bitmap to display. ie, which rotation. loopN = 0; // step 4. this.addEventListener(Event.ENTER_FRAME,animateBitmapsF); } function animateBitmapsF(e:Event=null):void { // Applying the lock() method stops the bitmap from being // updated while its bitmapData object is being updated. // I’ll unlock when all the bitmapData changes are completed // so the bitmap can then be updated. displayBMPD.lock(); // This is where the pixels in displayBMPD are "erased". Except, // they are not erased. They are all colored bgColor, which is // the background color I chose. displayBMPD.fillRect(displayBG_Rect,bgColor); // This code is similar to the MovieClip example except instead // of using mcA to update MovieClips, I am using dataA and // updating data. for(i=num-1;i>=0;i–){ pt = Point(dataA[i][0]); // This is quirky. If I do not perform some arithmetic // operation(s) (like multiplying by 1), memory use // increases significantly because of each of these two // lines of code. pt.x += 1*dataA[i][1]; pt.y += 1*dataA[i][2]; if (pt.x>stageW-diagL || pt.x<0) { dataA[i][1] *= -1; } if (pt.y>stageH-diagL|| pt.y<0) { dataA[i][2] *= -1; } // During each for-loop iteration, pixels are copied from // bmpdA to displayBMPD. You can see how loopN is used to // make it appear that squares are rotating and you can see 227
228 Chapter 7 n Optimizing Game Performance // how pt is used to make it appear as if each square is // also moving across the stage. displayBMPD.copyPixels(bmpdA[loopN%bmpdAL],bmpRect,pt); } loopN++; // all the pixels, for this Event.ENTER_FRAME loop iteration, have // been applied to displayBMPD. Finally, apply the unlock() method // so the bitmap corresponding to this bitmapData object is updated. displayBMPD.unlock(); } We set the frame rate at 60 to get a better idea of how much improvement we can see with blitting. The second MovieClip version plays at 21 fps (and 48 MB memory use), while the blitting version plays at 55 fps (and 6 MB memory use). So, the frame rate almost tripled with blitting, and far less memory was used. More importantly, the MovieClip version fails an eyeball test, while the blitting version passes with what appears to be very smooth animation. The downside to stage blitting, other than the difficulty coding, is that it may consume a large amount of memory to create the needed bitmaps. That is a significant factor when you’re creating a game for a device like the iPad that has high screen resolution (1024 × 768 for the first- and second-generation iPads, and 2048 × 1536 for the third-generation iPad) and relatively low memory (RAM) capacity (256 MB, 512 MB, and 1 GB for first-, second-, and third-generation iPads, respectively). In the Stage3D section, we’ll see how easy it is to consume large amounts of memory when using blitting in an effort to decrease CPU use. This will add more support to the memory versus CPU load tradeoff mentioned at the start of this chapter. Your game should consume no more than half the available RAM, and that includes not just RAM used by bitmaps but everything else in your game that consumes RAM. Partial Blitting As the name implies, partial blitting combines the use of the Flash display list and copying pixels to BitmapData objects. Typically, each object displayed on-stage is a bitmap that is added to the display list and manipulated like you’re used to doing with display objects like MovieClips. Each object’s animation is blitted to an array of BitmapData objects. For example, using the previous example of squares rotating and moving across the stage, we can blit the squares and their various rotations, store those BitmapData objects in bmpdA, add bitmaps to the display list, manipulate the bitmaps just like any display object (in other words, like the above MovieClips) in the Event.
Managing CPU/GPU Usage loop, and finally assign the appropriate bmpdA element. ENTER_FRAME bitmapData property of the bitmaps to the // 26 fps, 75mb with vectors // 24 fps, 75mb with arrays // fps test partial blitting import flash.events.Event; import flash.display.MovieClip; import com.kglad.MT; import flash.display.Bitmap; import flash.display.BitmapData; MT.init(this,3); var num:int=10000; var i:int; var rectSide:int = 2; var rectHalf:Number = rectSide/2; var rotationRate:int = 1; var dataA:Vector.<Array> = new Vector.<Array>(num); //var dataA:Array = new Array(num); var datumA:Array; var loopN:int var bmpdA:Vector.<BitmapData> = new Vector.<BitmapData>(int(360/rotationRate)); //var bmpdA:Array = new Array(int(360/rotationRate)); var diagL:Number = Math.sqrt(2*rectSide*rectSide); var diagLHalf:Number = diagL/2; var bmpdAL:int = bmpdA.length; var stageW:int = stage.stageWidth; var stageH:int = stage.stageHeight; var speed:Number; var initialDirection:Number; initF(); bitmapDataF(); // The only significant change here is datumA[0] is a Bitmap. // Its bitmapData property will be assigned in animateBitmapsF(). function initF():void { for (i=num-1;i>=0;i– –) { datumA = []; // Create the Bitmaps that will be added to the // display list datumA[0] = new Bitmap(null,"auto",true); // Add to the display list addChild(datumA[0]); // Assign initial positions datumA[0].x = int(Math.random()*(stageW-diagL)); 229
230 Chapter 7 n Optimizing Game Performance datumA[0].y = int(Math.random()*(stageH-diagL)); // use speed and initialDirection to define // vectorX and vectorY speed = 1+2*Math.random(); initialDirection = 2*Math.PI*Math.random(); // store vectorX and vectorY datumA[1] = speed*Math.cos(initialDirection); datumA[2] = speed*Math.sin(initialDirection); dataA[i] = datumA; } } // No significant change in bitmapDataF() function bitmapDataF():void{ var mc:MovieClip = new MovieClip(); with(mc.graphics){ beginFill(0xaa0000); drawRect(-rectHalf,-rectHalf,rectSide,rectSide); endFill(); } var mat:Matrix = new Matrix(); for (i=0; i<360; i+=rotationRate) { var bmpd:BitmapData = new BitmapData(diagL,diagL,true,0x00ffff00); mat.identity(); mat.rotate(i*Math.PI/180); mat.tx+=diagLHalf; mat.ty+=diagLHalf; bmpd.draw(mc,mat); bmpdA[i] = bmpd; } loopN = 0; this.addEventListener(Event.ENTER_FRAME,animateBitmapsF); } function animateBitmapsF(e:Event=null):void { for (i=num-1;i>=0;i– –) { // Update the Bitmaps’ positions directly // Multiplying by 1 makes no difference here. dataA[i][0].x += dataA[i][1]; dataA[i][0].y += dataA[i][2]; if (dataA[i][0].x>stageW-diagL || dataA[i][0].x<0) { dataA[i][1] *= -1; } if (dataA[i][0].y>stageH-diagL || dataA[i][0].y<0) { dataA[i][2]*=-1; }
Managing CPU/GPU Usage // Assign the bitmapData property for each Bitmap. dataA[i][0].bitmapData = bmpdA[loopN%bmpdA.length]; } loopN++; } For this example, partial blitting isn’t nearly as fast as stage blitting when tested on my PC. But keep an open mind, because partial blitting may be faster than stage blitting in some situations. In addition, it’s easier to code partial blitting than stage blitting, so if you can achieve acceptable frame rates with partial blitting, that would eliminate the additional work needed for stage blitting. Loops Event.ENTER_FRAME, for, while and do loops. Event.ENTER_FRAME Loops In the previous chapter, we found that creating multiple Event.ENTER_FRAME listeners applied to one instance calling multiple listener functions was slightly more efficient than creating one Event.ENTER_FRAME listener calling one listener function, which then called other functions. However, it’s a different situation when you have multiple objects, each with added Event.ENTER_FRAME listeners, compared to one Event.ENTER_FRAME listener. There is about a twofold performance gain using one object with Event.ENTER_FRAME listeners compared to using many objects with Event.ENTER_FRAME listeners. (See the enterFrame_ test_one_v_many_loops_with_different_movieclips folder.) For example, in the tank combat game, all of our tanks had their own Event.ENTER_FRAME listeners. This is less efficient than having one object with Event.ENTER_FRAME listeners updating all the tanks and shells. For Loops, While Loops, and Do Loops The bottom line on fast-executing loops in Flash is that reverse for loops are the fastest. If a stored list of same-type objects is needed in the loop, a reverse for loop using a vector to reference the list of objects is fastest. All three loops execute faster if you use an int for the iteration parameter than if you use a uint. All three loops execute faster if you decrement the loop variable than if you increment it. (Note: If you decrement a loop variable i and use i>=0 as the terminal condition, you will trigger an endless loop if i is a uint.) 231
232 Chapter 7 n Optimizing Game Performance All three loops execute faster if you use a variable or constant for the terminal condition rather than an expression or object property. For example: For(var i:int=0;i<some_array.length;i++){ } is slower than: Var len:int = some_array.length; for(var i:int=0;i<len;i++){ // } because Flash must evaluate the terminal condition with each loop iteration. Because the initial condition only needs to be evaluated once (and not with each loop iteration), there is no significant difference whether you use an expression or object property for the initial condition in a for loop. You should move anything outside a loop that can be moved without affecting the result. That includes declaring objects outside the loop (see “Object Reuse” within the “Objects” section), where using the new constructor inside a loop sometimes can be moved outside the loop and the terminal loop condition, if it is an expression, should be evaluated outside the loop. Again, reverse for loops are much faster than while loops and do loops. Using some simple tests, I found a reverse for loop to be 4 to 10 times faster than comparable while and do loops. (See /support files/Chapter 07/loops/for_v_do_v_while_loops.fla.) var i:int; var n:int = 100000000; var startTime:int = getTimer(); /* // 4162 i=n; while(i>=0){ i–; } */ ///* // 4177 i = n; do { i–; }while(i>=0);
Managing CPU/GPU Usage //*/ /* // 434 for(i=n-1;i>=0;i– –){ // } */ trace(getTimer()-startTime); I have seen some mention that using objects that each reference the next object is faster than using an array to reference the objects. For example, the following creates a sequence of MovieClips that allows referencing all n MovieClips by starting with last_mc and using the prev property to iterate through them all. In my tests I found that to be false. Using an array was easier and faster both to initialize and to use. Using a vector instead of an array, of course, was still faster. (See the test file in /support files/Chapter 07/loops/for_loop_v_sequential_loop.) var n:int = 500000; var i:int; var mc:MovieClip; var last_mc:MovieClip; var prev_mc:MovieClip; for(i=n-1;i>=0;i– –){ mc=new MovieClip(); if(i==n-1){ last_mc = mc; } else { prev_mc.prev = mc; } prev_mc = mc; } None of these suggestions is likely to make a major difference under most conditions, but they’re worth knowing if you’re trying to squeeze every bit of efficiency out of your coding or you need to iterate through a large number of loops. For example, in the blitting example, using constants instead of recalculating array lengths and iterating from end to start made no difference in the MovieClip frame rate but did increase the blitting frame rate by about 10 percent. Mouse Interactivity MovieClips and Sprites can interact with the mouse. Even when you don’t code for any mouse interactivity, the Flash Player checks for mouse interactions. You can 233
234 Chapter 7 n Optimizing Game Performance save some CPU cycles by disabling mouse interactivity for those objects that do not need to interact with the mouse. This can be a major help if you see a performance problem (or you detect that one of your computer’s fans increases speed) when your mouse moves across the stage. Disabling mouse interactivity will improve performance and quiet your computer fan(s). For example, none of my tanks interacted with the mouse, so, in the could use: Tank class, I // disables mouse interactivity for the tank this.mouseEnabled = false; // disables mouse interactivity for all children of the tank this.mouseChildren = false. I saw the frame rate increase by about 2 1/2 times when disabling all MovieClips in a test file. This code is in /support files/Chapter 07/mouse_interactivity. import com.kglad.MT; import flash.display.MovieClip; // fs test comparing movieclips with and without mouse enabling. MT.init(this,3); var num_obj:int=100000; // mc’s mouse enabled: fps~55 with no mouse movement. // fps<20 with mouse movement. // mc’s mouse disabled: fps~55 with no mouse movement. // fps~35 with mouse movement. // mc’s mouse disabled and main movieclip disabled: // fps~55 with no mouse movement. // fps~50 with mouse movement. var mouseEnable:Boolean=false; this.mouseEnabled=mouseEnable; this.mouseChildren=mouseEnable; for (var i:int=0; i<num_obj; i++) { var mc:MovieClip = new MovieClip(); addChild(mc); graphicF(mc); childrenF(mc); } function graphicF(mc:MovieClip):void { with (mc.graphics) { beginFill(0xffffff*Math.random()); drawRect(0,0,Math.ceil(2000/num_obj),Math.ceil(2000/num_obj)); }
Managing CPU/GPU Usage mc.x=stage.stageWidth*Math.random(); mc.y=stage.stageHeight*Math.random(); } function childrenF(mc:MovieClip):void { var c:MovieClip = new MovieClip(); mc.addChild(c); graphicF(c); mc.mouseEnabled=mouseEnable; mc.mouseChildren=mouseEnable; } Remove Event Listeners Even though the later Flash Player versions appear to remove listeners when objects are gc’d and having strong listeners doesn’t appear to delay gc’ing, you should still explicitly remove all event listeners as soon as possible. The sooner a listener is removed, the fewer CPU cycles are consumed in dealing with the listener. In addition, you may not know which Flash Player version a user has unless you publish for a mobile and package Air with your game. Some Player versions may not gc objects that have even weak listeners. In other words, don’t count on the Flash Player to save you from poor coding. Just like the problem I had with tank combat, listeners can continue to exist, cause havoc, and consume CPU cycles even when the listener is applied to an object that is ready for gc. Stage3D is a GPU-enabled display-rendering model that is new with Flash Player 11. This model was designed to facilitate 3D rendering but reportedly can be advantageously used for 2D displays, too. Stage3D Because display rendering typically has been handled by the CPU, which also does all the other work needed to run a game, harnessing the power of the GPU for rendering and freeing the CPU to do all the other work should significantly improve performance on devices with capable GPUs. To view Stage3D content, you will need to use Flash Player 11 or better. To use the Stage3D API, you will need to publish for Flash Player 11 or better. If you have Flash Pro CS6, you’re set. If you have Flash Pro CS5 or CS5.5, you can update your Flash Pro so you can publish to Flash Player 11. See http://blogs.adobe.com/rgalvan/2011/ 11/adding-fp11-support-to-flash-pro-cs5-and-cs5-5.html. 235
236 Chapter 7 n Optimizing Game Performance Unfortunately, using the Stage3D API is laborious and difficult. However, there are several free public frameworks available that handle the low-level laborious code needed to use Stage3D, and they all offer easier-to-use APIs. One of these frameworks, Starling (http://gamua.com/starling), is designed for 2D games. It is easy to use and effectively abstracts the complexity of Stage3D by supplying its own API (http://doc.starling-framework.org/core). I used Starling to see how it compared to blitting and partial blitting. In some situations it performed worse than both of those. In fact, it performed much worse than the unoptimized 10,000-square MovieClip test. However, unchecking Permit Debugging in the Starling test more than doubled the frame rate and was comparable to the unoptimized 10,000-square MovieClip test. That is still a disappointment, but part of the problem is that I used the debug version of the Flash Player, and Starling appears to perform much worse in the debug versus non-debug version of the Flash Player. In any case, although Starling fares poorly when compared to blitting in the 10,000 MovieClip test, I believe that’s because the 10,000 MovieClip test is particularly amenable to blitting. It may not be that Starling does badly at rendering the 10,000 MovieClip test. If you’re using many MovieClips that each contain a timeline with animation, Starling will almost certainly outperform anything you can build that utilizes simple optimizations. In addition, although blitting may provide the performance needed to meet or exceed the benefits of using Stage3D and Starling, blitting may not be practical because of the memory required to create the needed bitmaps. After the comparison of blitting and Starling using the 10,000 MovieClip test, I will apply blitting to animations from two Starling tests that I found online. These tests show substantial memory problems when applying blitting to those two animations. To use Starling, you need to download the starling.swc (http://gamua.com/starling/ download) and add it to your Library path by first clicking File > Publish Settings > ActionScript Settings. (See Figure 7.1.)
Managing CPU/GPU Usage Action script settings icon Figure 7.1 Click the ActionScript Settings icon to open the Advanced ActionScript Settings 3.0 panel, where you can add an SWC to your library path. Source: Adobe Systems Incorporated. Then click the Library Path tab and Browse to SWC File icon. (See Figures 7.2 and 7.3.) 237
238 Chapter 7 n Optimizing Game Performance Library Path tab Figure 7.2 Click the Advanced ActionScript Settings 3.0 Settings Library Path tab. Source: Adobe Systems Incorporated.
Managing CPU/GPU Usage Browse to SWC File icon Figure 7.3 Click the Browse to SWC File icon to open an Open File window. Source: Adobe Systems Incorporated. 239
240 Chapter 7 n Optimizing Game Performance Figure 7.4 Open File window. Source: © 2013 Keith Gladstien, All Rights Reserved. With the Open File window open, navigate to your downloaded starling.swc, click it, and then click Open. That should add starling.swc to your Library Path (see Figure 7.5).
Managing CPU/GPU Usage Confirmation that starling.swf has been added to your Library Path. Figure 7.5 After clicking on starling.swc, you should see it added to your Library Path. Source: Adobe Systems Incorporated. Finally, click OK to close the Advanced ActionScript 3.0 Settings panel and click OK again to close the Publish Settings panel. Save your FLA, and you’re ready to use the Starling API. I created a document class, Main, that passes a class (Game) to Starling. In Game, I create 10,000 rectangles. Only they are called quads in Starling, and I created a QuadSprite class to create those quads. 241
242 Chapter 7 n Optimizing Game Performance The test files are in /support files/Chapter 07/starling_test/test1. There are two test files in that directory, starling_test.fla and starling_test2.fla. The only difference is that starling_test.fla uses Main.as, which initializes its Starling instance using Game. as, while starling_test2.fla uses Main1.as, which initializes its Starling instance using Game1.as. The only difference between Game.as and Game1.as is the application of touchable and blendMode properties directly to the quads (Game.as) and parent sprite (Game1. as) in an attempt to increase performance. I found no significant benefit of one over the other. Starling Test with 10,000 MovieClips com.kglad.Main package com.kglad{ import flash.system.ApplicationDomain; import flash.display.Sprite; import starling.core.Starling; public class Main extends Sprite { private var starlingInst:Starling; public function Main() { // Check for Flash Player 11 if (ApplicationDomain.currentDomain.hasDefinition("flash.display.Stage3D")){ // It’s present so we can use Stage3D // and Starling startF(); } else { // Start fallback code if hardware acceleration is // not available } } private function startF():void{ // I ran into this problem while profiling this test. // If your game loses focus, for example, by opening // the CPU profiler, you get an error message when // returning to your game unless you take steps to // handle the lost context. Well, there are a few // steps to take in Stage3D, but in Starling there’s // just one thing to do. Enable the static // handleLostContext property. Starling.handleLostContext = true; // Create Starling instance by passing a first
Managing CPU/GPU Usage // parameter that is a class that extends a Starling // DisplayObject. This class is instantiated and // added to the Starling stage. The second // parameter that must be passed is a Flash // stage reference. starlingInst = new Starling(Game,stage); // Starling has a built-in frame rate display. starlingInst.showStats = true; // set anti-aliasing (higher is better quality // but slower performance) starlingInst.antiAliasing=0; starlingInst.enableErrorChecking = true; // Start the Starling instance starlingInst.start(); } } } com.kglad.Game package com.kglad{ // import the needed Starling classes import starling.display.Quad; import starling.display.Sprite; import starling.events.Event; import starling.utils.deg2rad; import starling.display.BlendMode; public class Game extends Sprite { private var quad:QuadSprite; private var pSprite:Sprite; private var num:int = 10000; private var i:int; private var rectSize:int = 2; private var rotationRate:Number = deg2rad(1); private var quadA:Vector.<QuadSprite> = new Vector.<QuadSprite>(num); private var stageW:int; private var stageH:int; private var rectHalf:Number = rectSize/2; private var speed:Number; private var quadVector:Vector.<Vector.<Number>> = new Vector.<Vector.<Number>>(num); public function Game() { // no weak listeners in starling addEventListener(Event.ADDED_TO_STAGE,init); 243
244 Chapter 7 n Optimizing Game Performance } private function init( e:Event ):void { removeEventListener(Event.ADDED_TO_STAGE,init); stageW = stage.stageWidth; stageH = stage.stageHeight; createQuadsF(); addEventListener(Event.ENTER_FRAME,animateQuadsF); } private function createQuadsF():void{ // create a parent starling sprite in case I want // to add TouchEvents. Instead of adding many // TouchEvents (one to each child), I’ll just // add one to the parent and, in the event // listener, detect which child was touched. parentF(); for(i=num-1;i>=0;i– –){ // A starling sprite that contains a quad. quad = new QuadSprite(rectSize,0xffffff*Math.random()); quadA[i] = quad; // quad rotation, speed, vectorX, vectorY // Starling DisplayObjects have rotations in // radians, not degrees // Because QuadSprites look the same when // rotated pi/2 and pi etc, I may as well // use a rotation that requires the // least computation. quad.rotation = randomF(0,Math.PI); speed = randomF(1,3); //quad.vectorX = speed*randomF(-1,1); //quad.vectorY = speed*randomF(-1,1); quadVector[i] = new <Number>[speed*randomF (-1,1),speed*randomF(-1,1)]; quad.blendMode = BlendMode.NONE; //quad.flatten(); quad.touchable = false pSprite.addChild(quad); quad.x = int(randomF(rectHalf,stageW-rectSize)); quad.y = int(randomF(rectHalf,stageH-rectSize)); } //pSprite.flatten(); } private function animateQuadsF(e:Event):void{ //pSprite.unflatten(); for(i=num-1;i>=0;i– –){ quadA[i].rotation += rotationRate;
Managing CPU/GPU Usage //quadA[i].x += quadA[i].vectorX; //quadA[i].y += quadA[i].vectorY; quadA[i].x += quadVector[i][0]; quadA[i].y += quadVector[i][1]; boundaryF(quadA[i],i); } //pSprite.flatten(); } private function boundaryF(q:QuadSprite,i:int):void{ if (q.x>stageW-rectHalf || q.x<rectHalf) { quadVector[i][0] *= -1; } if (q.y>stageH-rectHalf || q.y<rectHalf) { quadVector[i][1] *= -1; } } function parentF():void{ pSprite = new Sprite(); var pQuad:Quad = new Quad(stageW,stageH,0x000000); pQuad.blendMode = BlendMode.NONE; addChild(pSprite); pSprite.addChild(pQuad); } private function randomF(int1:int,int2:int):Number{ return int1+(int2-int1)*Math.random(); } } } com.kglad.QuadSprite package com.kglad { // import the needed Starling classes import starling.display.Quad; import starling.display.Sprite; // Extend the Starling Sprite public class QuadSprite extends Sprite { public function QuadSprite(size:int,color:uint) { var q:Quad = new Quad(size,size); q.pivotX = size/2; q.pivotY = size/2; q.color = color; addChild( q ); } } } 245
246 Chapter 7 n Optimizing Game Performance If you publish a mobile air game, set the Render mode to Direct (see Figure 7.6). If you publish embedding HTML, set the Window mode to Direct (see Figure 7.7). Render Mode combobox showing Direct is selected Figure 7.6 For a mobile air game, click File > Publish Settings > Player Settings and select Direct from the Render Mode combobox. Source: Adobe Systems Incorporated.
Managing CPU/GPU Usage Window Mode combobox showing direct is selected Figure 7.7 For a web-based game SWF that will be embedded in an HTML file, click File > Publish Settings > HTML Wrapper and select Direct from the Window Mode combobox. Source: Adobe Systems Incorporated. Starling Test with Eric Smith’s Patch the Scarecrow This test shows the drawback of blitting: large memory requirements. Using blitting to display a MovieClip with different rotations and different alpha values requires: (totalFrames × total rotations × total alphas) bitmaps If you’re going to display 360 rotations and 100 alphas, for a 10-frame 100-pixel × 100-pixel MovieClip, that would be: ~10 (frames) × 360 (rotations) × 100 (alphas) × 20,000 (pixels) × 32bits/pixel = ~27,466 MB 247
248 Chapter 7 n Optimizing Game Performance That is almost 27 gigabytes of memory, which exceeds the RAM of most desktops. And that’s just one MovieClip. In addition, it can take an appreciable amount of time to create the needed bitmaps. So, some projects cannot use blitting. Even though it’s not necessary to use 360 rotations (I used 120 in this example) and 100 different alphas (I used 26 in this example), I still used more memory than is available on even a third-generation iPad. The upside is that the example does run much faster on my desktop than Starling. This test is in /support files/Chapter 07/starling_test/test2/StarlingTest_blitting1.fla and compares Aymeric Lamboley’s Starling test at www.aymericlamboley.fr/blog/ starling-performance-test with blitting. This test uses Patch the Scarecrow from Citrus Engine’s Eric Smith (http:// citrusengine.com). import flash.display.MovieClip; import flash.geom.Point; import flash.display.Bitmap; import com.kglad.MT; import flash.geom.Matrix; import flash.geom.ColorTransform; MT.init(this,3); // num=600: fps ~60, memory ~1389mb // num=900: fps ~54 // num=1200:fps ~41 var bgColor:uint = 0x999999; // display bitmap where "frame" will be drawn. var displayBG_Rect:Rectangle = new Rectangle(0,0,stage.stageWidth, stage.stageHeight); var displayBMPD:BitmapData = new BitmapData(stage.stageWidth,stage.stageHeight, false,bgColor); var displayBMP:Bitmap = new Bitmap(displayBMPD); var dataA:Array=[]; var datumA:Array; var loopN:int var bmpdA:Array = []; var bmpRect:Rectangle; var num:int = 600; var patchW:Number; var patchH:Number; var bmpdALength:int; var stageW:int = stage.stageWidth; var stageH:int = stage.stageHeight;
Managing CPU/GPU Usage var deg2Rad:Number = Math.PI/180; var bmpd:BitmapData; var diagL:Number; var mat:Matrix; var pt:Point = new Point(); var frameN:int; var rotationN:int; var alphaN:int; var rotationInc:int = 3; var alphaInc:int = 4; var rotationNum:int = 360/rotationInc; var alphaNum:int = 1+100/alphaInc; bitmapDataF(); init(); function bitmapDataF():void{ var patch:MovieClip = new Patch(); patchW = patch.width; patchH = patch.height; diagL = Math.sqrt(patchW*patchW+patchH*patchH); bmpRect = new Rectangle(0,0,diagL,diagL); mat = new Matrix(); var ct:ColorTransform = new ColorTransform(); //var bmpdNum:int = 0; var patchTF:int = patch.totalFrames; for(frameN=0; frameN<patchTF; frameN++) { if(frameN>0){ patch.gotoAndStop(frameN+1); } bmpdA[frameN] = []; for(rotationN=0;rotationN<360;rotationN+=rotationInc){ bmpdA[frameN][rotationN] = []; adjustF(rotationN*deg2Rad); for(alphaN=0;alphaN<=100;alphaN+=alphaInc){ //bmpdNum++; bmpd = new BitmapData(diagL,diagL,true,0x00ffff00); ct.alphaMultiplier = alphaN/100; bmpd.draw(patch,mat,ct); bmpdA[frameN][rotationN][alphaN] = bmpd; } } } //trace("bmpdNum: ",bmpdNum,bmpdNum*diagL*diagL*4/(1024*1024)); 249
250 Chapter 7 n Optimizing Game Performance addChild(displayBMP); // loopN used to count number of enterframe loops and // choose which bitmap to display. ie, which patchMovieClip frame. loopN = 0; } function adjustF(r:Number):void{ mat.identity(); mat.rotate(r); mat.tx = diagL/2; mat.ty = diagL/2; } function init():void { bmpdALength = bmpdA.length; for (var i:int=num-1;i>=0; i–) { // = [initial point, speed, initial direction, // initial patchMovieClip frame // index] ///* dataA[i] = int(bmpdALength*Math.random()); //*/ } this.addEventListener(Event.ENTER_FRAME,animateBitmapsF); } function animateBitmapsF(e:Event):void { displayBMPD.lock(); displayBMPD.fillRect(displayBG_Rect,bgColor); for(i=num-1;i>=0;i– –){ ///* pt.x = -diagL/2+int(stageW*Math.random()); pt.y = -diagL/2+int(stageH*Math.random()); frameN = (loopN+dataA[i])%bmpdALength; rotationN = rotationInc*int(rotationNum*Math.random()); alphaN = alphaInc*int(alphaNum*Math.random()); //*/ displayBMPD.copyPixels(bmpdA[frameN][rotationN][alphaN],bmpRect,pt); } loopN++; displayBMPD.unlock(); } Starling Test with Chris Georgenes’ Mudbubble Boy This is another test comparing Starling with blitting. The Starling test is here: www.kouma.fr/lab/StarlingStressTest and uses Chris Georgenes’ Mudbubble Boy (www.mudbubble.com).
Managing CPU/GPU Usage This test shows an additional potential issue with blitting. The copyPixels method’s speed is inversely related to the size of the bitmaps being copied. You can test that by changing the definition of boyW and/or boyH (for example, boyW=boy.width/2). Also, with boy’s diagL ~333 px and boy having 15 frames, I had to decrease the rotations and alpha to prevent an Invalid BitmapData error. I was trying to use 15 frames × 120 rotations × 26 alphas × 333 px × 333 px × 32bits/ px > 19 GB, which exceeded my 12 GB of RAM (of which only 3 MB is usable by Flash Pro CS6). And, if I tried to incorporate scaling, even more memory would have been required. Using 12 rotations instead of 120 decreased memory usage by 1/10 to less than 2 GB of boy bitmaps and avoided the Invalid BitmapData error. This test is in /support files/Chapter 07/starling_test/test2/StarlingTest_blitting2.fla. import flash.display.MovieClip; import flash.geom.Point; import flash.display.Bitmap; import com.kglad.MT; import flash.geom.Matrix; import flash.geom.ColorTransform; MT.init(this,3); // num=600: FPS: 24 || Memory Use: 2015.79 MB var bgColor:uint = 0x999999; // display bitmap where "frame" will be drawn. var displayBG_Rect:Rectangle = new Rectangle(0,0,stage.stageWidth,stage. stageHeight); var displayBMPD:BitmapData = new BitmapData(stage.stageWidth,stage. stageHeight,false,bgColor); var displayBMP:Bitmap = new Bitmap(displayBMPD); var dataA:Array=[]; var datumA:Array; var loopN:int var bmpdA:Array = []; var bmpRect:Rectangle; var num:int = 600; var boyW:Number; var boyH:Number; var bmpdALength:int; var stageW:int = stage.stageWidth; var stageH:int = stage.stageHeight; var deg2Rad:Number = Math.PI/180; var bmpd:BitmapData; 251
252 Chapter 7 n Optimizing Game Performance var diagL:Number; var mat:Matrix; var pt:Point = new Point(); var frameN:int; var rotationN:int; var alphaN:int; var rotationInc:int = 30; var alphaInc:int = 4; var rotationNum:int = 360/rotationInc; var alphaNum:int = 1+100/alphaInc; bitmapDataF(); init(); function bitmapDataF():void{ var boy:MovieClip = new Boy(); boyW = boy.width; boyH = boy.height; diagL = Math.ceil(Math.sqrt(boyW*boyW+boyH*boyH)); bmpRect = new Rectangle(0,0,diagL,diagL); mat = new Matrix(); var ct:ColorTransform = new ColorTransform(); var bmpdNum:int = 0; var boyTF:int = boy.totalFrames; for(frameN=0; frameN<boyTF; frameN++) { if(frameN>0){ boy.gotoAndStop(frameN+1); } bmpdA[frameN] = []; for(rotationN=0;rotationN<360;rotationN+=rotationInc){ bmpdA[frameN][rotationN] = []; adjustF(rotationN*deg2Rad); for(alphaN=0;alphaN<=100;alphaN+=alphaInc){ bmpdNum++; bmpd = new BitmapData(diagL,diagL,true,0x00ffff00); ct.alphaMultiplier = alphaN/100; bmpd.draw(boy,mat,ct); bmpdA[frameN][rotationN][alphaN] = bmpd; } } } //trace("bmpdNum: ",bmpdNum,bmpdNum*diagL*diagL*4/(1024*1024)); addChild(displayBMP); // loopN used to count number of enterframe loops and choose
Managing CPU/GPU Usage // which bitmap to display. ie, which boyMovieClip frame. loopN = 0; } function adjustF(r:Number):void{ mat.identity(); mat.rotate(r); mat.tx = diagL/2; mat.ty = diagL/2; } function init():void { bmpdALength = bmpdA.length; for (var i:int=num-1;i>=0; i–) { // = [initial point, speed, initial direction, // initial boyMovieClip frame // index] ///* dataA[i] = int(bmpdALength*Math.random()); //*/ } this.addEventListener(Event.ENTER_FRAME,animateBitmapsF); } function animateBitmapsF(e:Event):void { displayBMPD.lock(); displayBMPD.fillRect(displayBG_Rect,bgColor); for(i=num-1;i>=0;i– –){ ///* pt.x = -diagL/2+int(stageW*Math.random()); pt.y = -diagL/2+int(stageH*Math.random()); frameN = (loopN+dataA[i])%bmpdALength; rotationN = rotationInc*int(rotationNum*Math.random()); alphaN = alphaInc*int(alphaNum*Math.random()); //*/ displayBMPD.copyPixels(bmpdA[frameN][rotationN][alphaN],bmpRect,pt); } loopN++; displayBMPD.unlock(); } You can avoid excessive memory requirements because of needed rotations and alphas by using partial blitting because the rotations and alphas are applied directly to the bitmaps, avoiding the need for the additional BitmapData objects. Also, each of the BitmapData objects used is much smaller because it doesn’t need to account for the rotations. However, partial blitting isn’t nearly as efficient as blitting, so tests using Patch and Mudbubble Boy show the benefit of using Starling over both blitting techniques. 253
254 Chapter 7 n Optimizing Game Performance The partial blitting test using Patch is in /support files/Chapter 07/starling_test/test2/ StarlingTest_partial_blitting.fla, and the partial blitting test using Mudbubble Boy is in /support files/Chapter 07/starling_test/test2/StarlingTest_partial_blitting1.fla. Here is the code using Mudbubble Boy. import flash.display.MovieClip; import flash.geom.Point; import flash.display.Bitmap; import com.kglad.MT; import flash.geom.Matrix; MT.init(this,1); // FPS: 1 || Memory Use: 14.89 MB var dataA:Array=[]; var datumA:Array; var loopN:int var bmpdA:Array = []; var bmpRect:Rectangle; var num:int = 600; var boyW:Number; var boyH:Number; var bmpdALength:int; var stageW:int = stage.stageWidth; var stageH:int = stage.stageHeight; bitmapDataF(); init(); function bitmapDataF():void{ var boy:MovieClip = new Boy(); boyW = boy.width; boyH = boy.height; //bmpRect = new Rectangle(0,0,boyW,boyH); var mat:Matrix = new Matrix(); mat.tx = boyW/2; mat.ty = boyH/2; for (var i:int=0; i<boy.totalFrames; i++) { boy.gotoAndStop(i); var bmpd:BitmapData = new BitmapData(boyW,boyH,true,0x00ff0000); bmpd.draw(boy,mat); bmpdA[i] = bmpd; } bmpdALength = bmpdA.length; // loopN used to count number of enterframe loops and choose // which bitmap to display. ie, which boyMovieClip frame. loopN = 0; }
Managing CPU/GPU Usage function init():void { var diagL:Number = Math.sqrt(boyW*boyW+boyH*boyH); for (var i:int=num-1;i>=0; i–) { datumA = []; // Create the Bitmaps that will be added to the display list datumA[0] = new Bitmap(null,"auto",true); // Add to the display list addChild(datumA[0]); // Assign initial positions datumA[0].x = diagL+int(Math.random()*(stageH-2*diagL)); datumA[0].y = diagL+int(Math.random()*(stageH-2*diagL)); datumA[0].alpha = Math.random(); // use speed and initialDirection to define vectorX and vectorY speed = 5+10*Math.random(); initialDirection = -Math.PI/2+Math.PI*Math.random(); datumA[0].rotation = 180*initialDirection/Math.PI; // store vectorX and vectorY datumA[1] = speed*Math.cos(initialDirection); datumA[2] = speed*Math.sin(initialDirection); dataA[i] = datumA; } this.addEventListener(Event.ENTER_FRAME,animateBitmapsF); } function animateBitmapsF(e:Event):void { //this.removeEventListener(Event.ENTER_FRAME,animateBitmapsF); for(i=num-1;i>=0;i– –){ dataA[i][0].x += 1*dataA[i][1]; dataA[i][0].y += 1*dataA[i][2]; dataA[i][0].alpha = Math.random(); if (dataA[i][0].x>stageW-boyW || dataA[i][0].x<boyW) { dataA[i][0].x -= 1*dataA[i][1]; dataA[i][1] *= -1; dataA[i][0].rotation = 180-dataA[i][0].rotation; } if (dataA[i][0].y>stageH-boyW || dataA[i][0].y<boyW) { dataA[i][0].y -= 1*dataA[i][2]; dataA[i][2]*=-1; dataA[i][0].rotation *= -1; } dataA[i][0].bitmapData = bmpdA[loopN%bmpdALength]; } loopN++; } 255
256 Chapter 7 n Optimizing Game Performance Type All Variables Specify the class type of all variables. In addition to getting more compiler help detecting errors, code with typed variables runs faster than code with untyped variables. For example, the following code: var i:int; var n:int = 10000000; var sum:int = 0; var startTime:int = getTimer(); for(i=0;i<n;i++){ sum+=i; } trace(getTimer()-startTime,sum,n*(n-1)/2); runs about five times faster than: var i:*; var n:* = 10000000; var sum:* = 0; var startTime:* = getTimer(); for(i=0;i<n;i++){ sum+=i; } trace(getTimer()-startTime,sum,n*(n-1)/2); Use Vectors Instead of Arrays See the test files in the array_v_vector folder. array_vector_arithmetic.fla // small benefit of vectors over arrays confirmed. var n:int = 1000000; var i:int; var startTime:int; var sum:Number; ///* // array test startTime = getTimer(); var a:Array = new Array(n); for(i=0;i<n;i++){ a[i] = int(Math.random()*n); } sum = 0;
Managing CPU/GPU Usage for(i=0;i<n;i++){ sum+=a[i]; } trace("array test duration:",getTimer()-startTime,"|| ave = ",int(sum/n),": ave predicted = ",n/2); // output: array test: 366 || ave = 500049 : ave predicted = 500000 //*/ ///* // vector test startTime = getTimer(); var pool:Vector.<int> = new Vector.<int>(n); for(i=0;i<n;i++){ pool[i] = int(Math.random()*n); } sum = 0 for(i=0;i<n;i++){ sum+=pool[i]; } trace("vector test duration:",getTimer()-startTime,"|| ave = ",int(sum/n),": ave predicted = ",n/2); // output: vector test: 168 || ave = 499695 : ave predicted = 500000 //*/ array_v_vector_MovieClips.fla // small benefit of vectors over arrays confirmed import flash.display.MovieClip; var mc:MovieClip var n:int = 100000; var i:int; var startTime:int; ///* // array test startTime = getTimer(); var a:Array = new Array(n); for(i=0;i<n;i++){ a[i] = new MovieClip(); } for(i=0;i<n;i++){ mc = a.pop(); } trace("array test duration:",getTimer()-startTime); // output: array test: 750 //*/ 257
258 Chapter 7 n Optimizing Game Performance ///* // vector test startTime = getTimer(); var pool:Vector.<MovieClip> = new Vector.<MovieClip>(n); for(i=0;i<n;i++){ pool[i] = new MovieClip(); } for(i=0;i<n;i++){ mc = pool.pop(); } trace("vector test duration:",getTimer()-startTime); // output: vector test: 590 //*/ Summary Remember those five steps the Flash Player executes 24 times per second in a game with 24 fps? Each of the optimization techniques reviewed in this chapter will help minimize the time needed by the Flash Player to complete one or more of those steps.
Chapter 8 Developing and Distributing Games for iOS Devices In this chapter we’ll examine the steps to develop, test, and deploy a game for iOS (iPhone and iPad) devices. We’ll start with the tank combat game from Chapter 6, “Developing a Flash Game,” and adapt it to work on an iPad. After going through the code for the iPad tank combat game, we’ll cover some technical details: namely, how to test, publish, and distribute a game developed for an iOS device. Those topics follow the iPad tank combat game code and are the most important parts of this chapter. The Tank Combat Game for the iPad The first thing we need to change is all the mouse event listeners, because tablets and mobiles don’t have a mouse. That said, MouseEvent.MOUSE_DOWN events work well with touch devices, but MouseEvent.MOUSE_UP and MouseEvent.CLICK events don’t. So, for some games you can use MouseEvent.MOUSE_DOWN, and you won’t need to use any touch events. But for the tank combat game, we have sliders to drag and the player tank to control. We’ll use touch events for the sliders and the accelerometer to control the player tank. Also, the display size of the non-retina iPad is 1024 × 768 pixels with 20 pixels for the status bar, so we’ll change the stage size to 1024 × 748 and make changes to the _introView, _combatView, and _gameOverView symbols. We’ll also make our own slider because the slider component won’t work with touch events, and we’ll resize our button and check box to make them easier to use for those with less slender fingertips. 259
260 Chapter 8 n Developing and Distributing Games for iOS Devices There is a possibility of major problems with memory management, and we want to eliminate the need for gc’ing that might occur during combat, when a momentary decrease in performance would be problematic. So we’ll use pool classes for IntroView, CombatView, and GameOverView, as well as the player tanks and enemy tanks. I’m also concerned about the performance hit from having each tank use its own listeners, so we’ll use a Controller class to handle the movement, aiming, and firing of all the tanks and a controller pool to create and furnish the Controller class when it is needed. Event.ENTER_FRAME In addition, we’ll add code to prevent tanks from colliding, and we’ll remove the slider to control rotation in case we need to use partial blitting to get decent performance. (As it turns out, we won’t need to use blitting, but let’s leave the slider out anyway because it doesn’t add much to the game.) We’ll also add a restriction on how frequently a ShotSound can play so we can avoid the 32 sound-channel limit imposed by the Flash Player. That is a pretty generous limit, but we could easily exceed it by increasing the number of enemy tanks and the number of shells each tank can have on-screen at one time. This is especially noticeable at the start of combat, when all the EnemyTank instances fire. That would cause all sounds to stop working until enough sounds ended to clear the buffer. The files are in /support files/Chapter 8/tank_combat_iOS. There are many changes from version 9, starting with Main. I’ve commented the code to explain the changes made in this iOS version. The Main class initializes data and handles adding and removing all the views to the display list. Main package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.utils.getTimer; public class Main extends MovieClip { private var introView:IntroView; private var combatView:CombatView; private var gameOverView:GameOverView; public function Main() { // MT.init(this,3); // Initialize all variables to default values Data.resetAllF(); // Initialize all the views and tanks initializePoolsF();
The Tank Combat Game for the iPad addIntroViewF(); } private function initializePoolsF():void{ // Twenty EnemyTank instances are created in // EnemyTank_Pool. The EnemyTank’s parent (arena_mc) // width and height are hard-coded. EnemyTank_Pool.init(20,980,620); // Two PlayerTank instances are created in // PlayerTank_Pool. One for introView and one for // combatView. The first two parameters are the width // and height of the PlayerTank’s introView parent // (playerBG_mc), and the second two parameters are // the width and height of PlayerTank’s combatView // parent (arena_mc). PlayerTank_Pool.init(354,460,980,620); // Each of the views and Controller is created in // its pool classes. IntroView_Pool.init(1); CombatView_Pool.init(1); GameOverView_Pool.init(1); Controller_Pool.init(1); } private function addIntroViewF():void{ // IntroView instance retrieved from IntroView_Pool. // Similarly, for the other views. IntroView_Pool is // below so you can see how IntroView_Pool.retrieveF() // and IntroView_Pool.returnF() work. introView = IntroView_Pool.retrieveF(); introView.addEventListener("startCombatE",addCombatViewF,false,0,true); addChild(introView); } private function removeIntroViewF():void{ introView.removeEventListener("startCombatE",addCombatViewF,false); removeChild(introView); // Instead of readying introView for gc, it is returned // to IntroView_Pool so it is ready to be reused if the // game is replayed. Likewise, for the other views, as // you can see in the code to retrieve and return // combatView and gameOverView. IntroView_Pool.returnF(introView); } private function addCombatViewF(e:Event):void{ 261
262 Chapter 8 n Developing and Distributing Games for iOS Devices removeIntroViewF(); combatView = CombatView_Pool.retrieveF(); combatView.addEventListener("endCombatE",addGameOverViewF,false,0,true); addChild(combatView); stage.focus = combatView; } private function removeCombatViewF():void{ combatView.removeEventListener("endCombatE",addGameOverViewF,false); removeChild(combatView); CombatView_Pool.returnF(combatView); } private function addGameOverViewF(e:Event):void{ removeCombatViewF(); gameOverView = GameOverView_Pool.retrieveF(); gameOverView.addEventListener("replayE",replayF,false,0,true); addChild(gameOverView); } private function removeGameOverViewF():void{ gameOverView.removeEventListener("replayE",replayF,false); removeChild(gameOverView); GameOverView_Pool.returnF(gameOverView); } private function replayF(e:Event):void{ removeGameOverViewF(); addIntroViewF(); } } } The IntroView class allows the player to customize the game. IntroView package com.kglad { import flash.display.MovieClip; import flash.events.TouchEvent; import flash.events.MouseEvent; import flash.events.Event; import flash.ui.Multitouch; import flash.ui.MultitouchInputMode; import flash.sensors.Accelerometer; import flash.events.AccelerometerEvent;
The Tank Combat Game for the iPad public class IntroView extends MovieClip { // An array of all movieclips that contain a slider, // defined in init(). Used to ease coding for the slider // mouse listeners and for assigning the initial values // of the sliders. private var mc_sliderA:Array; private var controlsA:Array; private var sliderMinMaxA:Array; private var controller:Controller; private var accelerometer:Accelerometer; private var accelerometerE:AccelerometerEvent; var player:PlayerTank public function IntroView() { // Note: in IntroView, CombatView, and GameOverView, // things that only need to execute once are in the // constructor. Things that need to reset, when one // of these classes is added to the display list, are // in init(). I want to use the iPad’s accelerometer // to control player movement. accelerometer = new Accelerometer(); controlsA = [forward,back,left,right]; mc_sliderA = [tankSpeed_mc,shellSpeed_mc,maxShells_mc,maxHits_mc,enemyNum_mc, enemyEvasiveness_mc,startTime_mc,combatEndDelay_mc]; sliderMinMaxA = [[1,10],[1,40],[1,10],[1,10],[1,20],[1,10],[0,10],[0,10]]; // There are two input modes. MultitouchInputMode.TOUCH_POINT // (when you only need to detect one touch at a time) and the // default MultitouchInputMode.GESTURE (when you need to // detect more than one touch point at a time, like when // zooming and rotating). Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT; this.addEventListener(Event.ADDED_TO_STAGE,init,false,0, true); // Clean up everything in this class when no longer needed. // That is, remove listeners and return PlayerTank and // Controller instances to their pools. this.addEventListener(Event.REMOVED_FROM_STAGE,cleanupF,false,0,true); } private function init(e:Event):void{ // Add listener for accelerometer changes. 263
264 Chapter 8 n Developing and Distributing Games for iOS Devices accelerometer.addEventListener(AccelerometerEvent.UPDATE, accelerometerF,false,0,true); listenersF(); addPlayerTankAndControllerF(); } // add listeners and assign default values private function listenersF():void{ for(var i:int=controlsA.length-1;i>=0;i– –){ // The TouchEvent class has several properties // I can use. The TouchEvent.TOUCH_OVER is similar // to the MouseEvent.MOUSE_OVER. There is also a // TouchEvent.TOUCH_ROLL_OVER, which is similar to // MouseEvent.ROLL_OVER, which dispatches repeat // events if children of the currentTarget undergo // the event. controlsA[i].addEventListener(TouchEvent.TOUCH_OVER,explanationF,false,0,true); // TouchEvent.TOUCH_TAP is analogous to the // MouseEvent.CLICK event. controlsA[i].addEventListener(TouchEvent.TOUCH_TAP,controlsF,false,0,true); } startCombat_mc.addEventListener(TouchEvent.TOUCH_TAP,startCombatF,false,0,true); // Slider listeners for(i=mc_sliderA.length-1;i>=0;i– –){ // Check the Slider class for min,max getters // and setters mc_sliderA[i].sl.min = sliderMinMaxA[i][0]; mc_sliderA[i].sl.max = sliderMinMaxA[i][1]; mc_sliderA[i].addEventListener(TouchEvent.TOUCH_OVER,explanationF,false,0,true); mc_sliderA[i].sl.addEventListener(TouchEvent.TOUCH_MOVE,sliderChangeF,false,0, true); setValuesF(i); } // Coding for the free-for-all movieclip button. freeForAll_mc.cbox.addEventListener(TouchEvent.TOUCH_TAP,freeForAllF,false,0, true); freeForAll_mc.addEventListener(TouchEvent.TOUCH_OVER,freeForAllExplanationF, false,0,true); setValuesF(-1); // Coding for the reset button that resets parameters // to default values.
The Tank Combat Game for the iPad reset_mc.addEventListener(TouchEvent.TOUCH_TAP,resetF,false,0,true); } // Initial values for the sliders and free-for-all. private function setValuesF(i:int):void{ if(i>-1){ mc_sliderA[i].sl.value = Data[Data.defaultVariableA[i+5].split("_")[1]]; mc_sliderA[i].tf.text = Data[Data.defaultVariableA[i+5].split("_")[1]]; } else { // freeForAll_mc.cbox has two frames. frame 1 // without the check mark, frame 2 with the check mark. if(Data[Data.defaultVariableA[0].split("_")[1]]){ freeForAll_mc.cbox.gotoAndStop(2); } else { freeForAll_mc.cbox.gotoAndStop(1); } } } // This is where the text for the IntroView TextField (tf) // is assigned. private function explanationF(e:TouchEvent):void{ var val:int = e.currentTarget.sl.value switch(e.currentTarget.name){ case "tankSpeed_mc": tf.text = "Controls tank speed from a minimum of "+e.currentTarget.sl.min+" to a maximum of "+e.currentTarget.sl.max+"\nCurrent tank speed is "+val; break; case "shellSpeed_mc": tf.text = "Controls tank shell speed from a minimum of "+e.currentTarget.sl.min+" to a maximum of "+e.currentTarget.sl.max+" \nCurrent tank shell speed is "+val; break; case "maxShells_mc": tf.text = "Controls maximum number of shells each tank can have on-screen at any one time. You can adjust from a minimum of "+e.currentTarget.sl.min+" to a maximum of "+e.currentTarget.sl.max+"\nCurrent tank shell maximum number is "+val; break; case "maxHits_mc": 265
266 Chapter 8 n Developing and Distributing Games for iOS Devices tf.text = "Controls number of shell hits each tank can withstand before destruction. You can adjust from a minimum of "+e.currentTarget.sl.min+" to a maximum of "+e.currentTarget.sl.max+"\nCurrent number of shell hits before destruction is "+val; break; case "enemyNum_mc": tf.text = "Controls the number of enemy tanks. You can adjust from a minimum of "+e.currentTarget.sl.min+" to a maximum of "+e.currentTarget.sl.max+"\nCurrent enemy number is "+val; break; case "enemyEvasiveness_mc": tf.text = "Controls the frequency of enemy tank evasive manuevers. You can adjust from a minimum of "+e.currentTarget.sl.min+" to a maximum of "+e.currentTarget.sl.max+"\nCurrent enemy tank evasiveness is "+val; break; case "startTime_mc": tf.text = "Controls the delay from the intial combat screen until the first shot is allowed. You can adjust from a minimum of "+e.currentTarget.sl.min+" second(s) to a maximum of "+e.currentTarget.sl.max+" \nCurrent delay is "+val; break; case "combatEndDelay_mc": tf.text = "Controls the delay from the end of combat until the End Game Screen. You can adjust from a minimum of "+e.currentTarget.sl.min+" second(s) to a maximum of "+e.currentTarget.sl.max+" \nCurrent delay is "+val; break; } } // Data class setters called when there is a slider change. private function sliderChangeF(e:Event):void{ var val:int = int(e.currentTarget.value); switch(e.currentTarget.parent.name){ case "tankSpeed_mc": tf.text = "Controls tank speed from a minimum of "+e.currentTarget.min+" to a maximum of "+e.currentTarget.max+"\nCurrent tank speed is "+val; Data.tankSpeed = val; e.currentTarget.parent.tf.text = val; break; case "shellSpeed_mc": tf.text = "Controls tank shell speed from a minimum of "+e.currentTarget.min+" to a maximum of "+e.currentTarget.max+"\nCurrent tank shell speed is "+val;
The Tank Combat Game for the iPad Data.shellSpeed = val; e.currentTarget.parent.tf.text = val; break; case "maxShells_mc": tf.text = "Controls maximum number of shells each tank can have on-screen at any one time. You can adjust from a minimum of "+e.currentTarget.min+" to a maximum of "+e.currentTarget.max+"\nCurrent tank shell maximum number is "+val; Data.maxShells = val; e.currentTarget.parent.tf.text = val; break; case "maxHits_mc": tf.text = "Controls number of shell hits each tank can withstand before destruction. You can adjust from a minimum of "+e.currentTarget.min+" to a maximum of "+e.currentTarget.max+"\nCurrent number of shell hits before destruction is "+val; Data.maxHits = val; e.currentTarget.parent.tf.text = val; break; case "enemyNum_mc": tf.text = "Controls the number of enemy tanks. You can adjust from a minimum of "+e.currentTarget.min+" to a maximum of "+e.currentTarget.max+"\nCurrent enemy number is "+val; Data.enemyNum = val; e.currentTarget.parent.tf.text = val; break; case "enemyEvasiveness_mc": tf.text = "Controls the frequency of enemy tank evasive manuevers. You can adjust from a minimum of "+e.currentTarget.min+" to a maximum of "+e.currentTarget.max+"\nCurrent enemy tank evasiveness is "+val; Data.enemyEvasiveness = val; e.currentTarget.parent.tf.text = val; break; case "startTime_mc": tf.text = "Controls the delay from the start of the combat screen until the first shot is allowed. You can adjust from a minimum of "+e.currentTarget.min+" second(s) to a maximum of "+e.currentTarget.max+"\nCurrent delay is "+val; Data.startTime = val; e.currentTarget.parent.tf.text = val; break; case "combatEndDelay_mc": tf.text = "Controls the delay from the end of combat until the End Game Screen. You can adjust from a minimum of 267
268 Chapter 8 n Developing and Distributing Games for iOS Devices "+e.currentTarget.min+" second(s) to a maximum of "+e.currentTarget.max+"\nCurrent delay is "+val; Data.combatEndDelay = val; e.currentTarget.parent.tf.text = val; break; } } private function accelerometerF(e:AccelerometerEvent):void{ // AccelerometerEvent is saved for use in controlsF() accelerometerE = e; } // The saved AccelerometerEvent’s accelerationX and // accelerationY properties change when the iPad is // pitched and rotated. The user can specify the // limits of pitch and rotation that should be used // to control player’s forward, back, left, and right // movement. See controlsExplanationF() below. private function controlsF(e:TouchEvent):void{ switch(e.currentTarget.name){ case "forward": Data[e.currentTarget.name] = accelerometerE.accelerationY; break; case "back": Data[e.currentTarget.name] = accelerometerE.accelerationY; break; case "left": Data[e.currentTarget.name] = accelerometerE.accelerationX; break; case "right": Data[e.currentTarget.name] = accelerometerE.accelerationX; break; } } private function controlsExplanationF(e:TouchEvent):void{ switch(e.currentTarget.name){ case "forward": tf.text = "Angle the top of your device, then click this button to assign the rotation to move your tank forward. Leave some dead space between the forward and back rotations so you can also stop your tank."; break;
The Tank Combat Game for the iPad case "back": tf.text = "Angle the top of your device, then click this button to assign the rotation to move your tank backward. Leave some dead space between the forward and back rotations so you can also stop your tank."; break; case "left": tf.text = "Rotate your device like a steering wheel to turn left and right. Rotate to the left and then click this button to assign the rotation to turn left. Leave some dead space between the left and right rotations so you can also go straight."; break; case "right": tf.text = "Rotate your device like a steering wheel to turn left and right. Rotate to the right and then click this button to assign the rotation to turn right. Leave some dead space between the left and right rotations so you can also go straight."; break; } } // freeForAll_mc.cbox clicked: private function freeForAllF(e:TouchEvent):void{ var mc:MovieClip = MovieClip(e.currentTarget); // If the check box’s frame 1 is displayed (unchecked), // goto frame 2 (checked) and update _freeForAll in Data // using the setter. Else, goto frame 1 and update // _freeForAll in Data using the setter. if(mc.currentFrame==1){ mc.gotoAndStop(2); Data.freeForAll = true; } else { mc.gotoAndStop(1); Data.freeForAll = false; } } // freeForAll_mc explanation. private function freeForAllExplanationF(e:TouchEvent):void{ tf.text = "Indicates whether combat is a free-for-all (all tanks fight each other) or not (it’s you against all enemy tanks)"; } // Reset all sliders and checkbox and parameters in Data private function resetF(e:TouchEvent):void{ Data.resetAllF(); for(var i:intmc_sliderA.length-1;i>=-1;i–){ setValuesF(i); 269
270 Chapter 8 n Developing and Distributing Games for iOS Devices } } private function addPlayerTankAndControllerF():void{ // PlayerTank_Pool has two non-interchangeable instances // of PlayerTank. One for IntroView and one for CombatView, // so the retrieve and return functions are different for // each. player = PlayerTank_Pool.retrieveIntroF(); positionF(playerBG_mc,player); // Retrieve the only Controller instance. controller = Controller_Pool.retrieveF(); // Initialize the controller by calling init() and passing // the PlayerTank instance and the array of enemies, which // is empty in IntroView. controller.init(player,[]); } private function positionF(parent_mc:MovieClip,mc:MovieClip):void { parent_mc.addChild(mc); mc.x = int(parent_mc.width/2); mc.y = int(parent_mc.height/2); } private function startCombatF(e:TouchEvent):void{ dispatchEvent(new Event("startCombatE")); } private function cleanupF(e:Event):void{ // Remove all the listeners created here and return pool // instances removeListenersF(); PlayerTank_Pool.returnIntroF(player); controller.dispatchEvent(new Event("cleanupE")); Controller_Pool.returnF(controller); } private function removeListenersF():void{ accelerometer.removeEventListener(AccelerometerEvent. UPDATE, accelerometerF,false); for(var i:int=controlsA.length-1;i>=0;i– –){ controlsA[i].removeEventListener(TouchEvent.TOUCH_OVER,explanationF,false); controlsA[i].removeEventListener(TouchEvent.TOUCH_TAP,controlsF,false); } startCombat_mc.removeEventListener(TouchEvent.TOUCH_TAP,startCombatF,false);
The Tank Combat Game for the iPad for(i=mc_sliderA.length-1;i>=0;i– –){ mc_sliderA[i].removeEventListener(TouchEvent.TOUCH_OVER,explanationF,false); mc_sliderA[i].sl.removeEventListener(TouchEvent.TOUCH_MOVE,sliderChangeF,false); } freeForAll_mc.cbox.removeEventListener(TouchEvent.TOUCH_TAP,freeForAllF,false); freeForAll_mc.removeEventListener(TouchEvent.TOUCH_OVER,freeForAllExplanationF, false); reset_mc.removeEventListener(TouchEvent.TOUCH_TAP,resetF, false); } } } The IntroView_Pool class supplies the IntroView instance. IntroView_Pool package com.kglad{ public class IntroView_Pool { private static var pool:Vector.<IntroView>; public static function init(poolSize:int):void { // The next line creates a vector with length = poolSize // and populates the vector with IntroView instances // (that are null). That means if you use // pool.push(new IntroView()), pool.length = // 2*poolSize and half of pool’s elements will // be null. pool = new Vector.<IntroView>(poolSize); for(var i:int=poolSize-1;i>=0;i++){ pool[i] = new IntroView(); } } public static function retrieveF():IntroView { if (pool.length>0) { return pool.pop(); } else { // This branch should not execute. return new IntroView(); } } public static function returnF(view:IntroView):void { pool.push(view); 271
272 Chapter 8 n Developing and Distributing Games for iOS Devices } } } The Slider class is needed because the Flash Slider component didn’t work on the iPad. Slider package com.kglad { import import import import import import flash.display.MovieClip; flash.ui.Multitouch; flash.ui.MultitouchInputMode; flash.events.TouchEvent; flash.events.Event; flash.geom.Rectangle; public class Slider extends MovieClip { private var leftX; private var rightX; private var _value:Number; private var _max:Number; private var _min:Number; public function Slider() { // This is a horizontal slider so define the left and // right extremes of the thumb (which has a center x // registration point). When the slider // thumb.x = leftX, _value should be _min. When // the slider thumb.x = rightX, _value should be _max. // Linear interpolation is used to determine _value when // thumb.x is between leftX and rightX. leftX = this.track_mc.getRect(this).x; rightX = this.track_mc.getRect(this).width; Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT; this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ // Two other touch events defined exactly like you // would expect this.thumb.addEventListener(TouchEvent.TOUCH_BEGIN, touchBeginF); this.thumb.addEventListener(TouchEvent.TOUCH_MOVE, touchMoveF);
The Tank Combat Game for the iPad this.thumb.addEventListener(TouchEvent.TOUCH_END, touchEndF); } private function touchBeginF(e:TouchEvent):void{ // TouchEvents have a touchPointID, which is necessary // to keep track of different simultaneous touch points // when Multitouch.inputMode = MultitouchInputMode.GESTURE. // With single touch points like in this game, keeping // track of touch points is not critical, but it is // required for the startTouchDrag() and stopTouchDrag() // methods. The other parameters in startTouchDrag() are // the same as startDrag(). e.currentTarget.startTouchDrag(e.touchPointID, false, new Rectangle(0,0,this.rightX,0)); } private function touchMoveF(e:TouchEvent):void{ // Update _value valueF(); } private function touchEndF(e:TouchEvent):void{ valueF(); e.currentTarget.stopTouchDrag(e.touchPointID); } private function valueF():void{ // Linear interpolation to find _value given thumb.x _value = (_min*rightX-_max*leftX+(_max_min)* this.thumb.x)/(rightX-leftX); } public function get value():Number{ return _value; } public function set value(n:Number):void{ _value = n; // Another use of linear interpolation to find // thumb.x given _value this.thumb.x = (_value*(rightX-leftX)-_min*rightX_max*leftX)/ (_max-_min); } public function set max(n:Number):void{ _max = n; } public function get max():Number{ return _max; } 273
274 Chapter 8 n Developing and Distributing Games for iOS Devices public function set min(n:Number):void{ _min = n; } public function get min():Number{ return _min; } } } The CombatView class handles the actual tank combat. CombatView package com.kglad { import flash.display.MovieClip; import flash.display.DisplayObject; import flash.events.Event; import flash.utils.Timer; import flash.events.TimerEvent; import flash.utils.getTimer; public class CombatView extends MovieClip { // Variables that were initialized here have been moved to // init() so they can be reinitialized each time this is added // to the display list. private var player:PlayerTank; private var enemyNum:int; private var enemyA:Array = []; private var maxHits:int; private var timeTilGameOverView; private var startTime:int; // This is a new variable for the Controller class. private var controller:Controller; private var i:int; public function CombatView() { this.addEventListener(Event.ADDED_TO_STAGE,init,false,0, true); this.addEventListener(Event.REMOVED_FROM_STAGE,removedF,false,0,true); } private function init(e:Event):void{ // Initialize variables enemyNum = Data.enemyNum; maxHits = Data.maxHits; startTime = Data.startTime; timeTilGameOverView = 1000*Data.combatEndDelay;
The Tank Combat Game for the iPad // Initialize the stats display. statsDisplayF(); // Add player tank and enemy tank(s) addTanksF(); } private function statsDisplayF():void{ // Initialize the display of player score and player life score_tf.text = "0"; life_tf.text = "100%"; } private function addTanksF():void{ player = PlayerTank_Pool.retrieveCombatF(); // Add listeners for events dispatched from Tank to // remove player from the arrays when destroyed, // register a hit to player and when player "scores" // or hits another tank player.addEventListener("playerHitE",playerHitF,false,0, true); player.addEventListener("scoreE",scoreF,false,0,true); // Retrieve maxHits from Data maxHits = Data.maxHits; player.name = "player"; positionF(arena_mc,player); player.prevX = player.x; player.prevY = player.y; // Add enemy tank(s) for(i=enemyNum-1;i>=0;i– –){ var enemy:EnemyTank = EnemyTank_Pool.retrieveF(); enemy.name = i.toString(); enemy.foeA.push(player); positionF(arena_mc,enemy); enemy.prevX = enemy.x; enemy.prevY = enemy.y; enemyA.push(enemy); } // all enemies are player foes. player.foeA = player.foeA.concat(enemyA); // player has no friends. So, I did not define a // player.friendA. You’re on your own. // There is a Data.freeForAll parameter chosen by // the user to indicate a free-for-all if(Data.freeForAll){ for(i=enemyNum-1;i>=0;i– –){ // Add all enemies to each enemy’s foeA 275
276 Chapter 8 n Developing and Distributing Games for iOS Devices enemyA[i].foeA = enemyA[i].foeA.concat(enemyA); // Remove enemyA[i] so no one is // their own foe. enemyA[i].foeA.splice(i+1,1); } } else { for(i=enemyNum-1;i>=0;i– –){ // add all enemies to each enemy’s friendA enemyA[i].friendA = enemyA[i].friendA. concat(enemyA); // remove enemyA[i] so no one is their own // friend. At this point, no one targets // friends, but if a shell hits a friend, // it counts. Later I may add code to check // that no shot is apt to hit a friend. enemyA[i].friendA.splice(i,1); } } // All tanks created. Start countdown to combat: startTimeF(startTime); // all modifications to enemyA done in Controller // affect enemyA here and vice versa. //controller = new Controller(player,enemyA,test_tf); controller = Controller_Pool.retrieveF(); controller.init(player,enemyA); } // I moved tanks further from the boundary. private function positionF(arena_mc:MovieClip,mc:MovieClip):void { arena_mc.addChild(mc); mc.x = int(2*mc.width+(arena_mc.width-4*mc.width)* Math.random()); mc.y = int(2*mc.height+(arena_mc.height-4*mc.height)* Math.random()); // If this is an enemy tank, make sure it’s not // hitting another tank. player is added first, // so no need to check for a hit when it’s added. //trace(mc.name); if(mc!=player){ if(player.hitTestObject(mc)){ positionF(arena_mc,mc); } else { // check mc doesn’t hit any enemyA element for(var i:int=enemyA.length-1;i>=0;i– –){
The Tank Combat Game for the iPad if(mc.hitTestObject(enemyA[i])){ positionF(arena_mc,mc); break; } } } } } // scoreF called via player scoreE event listener when // player scores a hit on an enemy tank private function scoreF(e:Event=null):void{ // Update stats display score_tf.text = Data.score.toString(); if(enemyA.length == 0){ // If there are no more enemies, the game // is over and player won. This function is // where I delay removing this view and // adding game over view. delayLeavingCombatViewF(); } } // playerHitF called via player playerHitE event listener // and possibly from removeTankF() private function playerHitF(e:Event = null):void{ // This is where maxHits is used to update the // stats display life_tf.text = int(100*(maxHits-Data.playerHits)/maxHits)+" %"; if(Data.playerHits==maxHits){ // Game over. you lost. // Same delay function executes whether player // wins or loses. delayLeavingCombatViewF(); } } ///////////////////// // start combat delay private function startTimeF(n:int):void{ // Display time until start startTime_tf.text = n.toString(); // Create timer to count down the start time // if startTime>0. 277
278 Chapter 8 n Developing and Distributing Games for iOS Devices if(n>0){ var startTimer:Timer = new Timer(1000,n); startTimer.addEventListener(TimerEvent.TIMER,timerF,false,0,true); startTimer.start(); } } private function timerF(e:TimerEvent):void{ // Update start time countdown startTime_tf.text = (e.target.repeatCount-e.target.current Count).toString(); if(e.target.currentCount==e.target.repeatCount){ // Remove the listener when no longer needed. e.target.removeEventListener(TimerEvent.TIMER,timerF,false); } } // end combat delay private function delayLeavingCombatViewF():void{ // This fixes that problem where player could be // destroyed, but in free-for-all mode, enemy tanks // could keep destroying each other, leading to // possibly erroneous results being displayed in // the GameOverView instance. Data.enemiesRemaining // used in GameOverView accurately indicating the // number of enemies left when player destroyed. Data.enemiesRemaining = enemyA.length; var delayTimer:Timer = new Timer(timeTilGameOverView,1); delayTimer.addEventListener(TimerEvent.TIMER,endCombatF,false,0,true); delayTimer.start(); } ///////////////////// // Finally, dispatch the endCombatE event so Main can // remove the CombatView instance and add a GameOverView // instance. private function endCombatF(e:Event):void{ e.target.removeEventListener(TimerEvent.TIMER,endCombatF, false); dispatchEvent(new Event("endCombatE")); } private function removedF(e:Event):void{ // Do not remove this event listener //this.removeEventListener(Event.REMOVED_FROM_STAGE,removedF,false); if(player.hasEventListener("playerHitE")){
The Tank Combat Game for the iPad player.removeEventListener("playerHitE",playerHitF,false); } // Remove this event listener if(player.hasEventListener("scoreE")){ player.removeEventListener("scoreE",scoreF,false); } for(var i:int=arena_mc.numChildren-1;i>=0;i– –){ var c:DisplayObject = arena_mc.getChildAt(i); if(c is MovieClip){ arena_mc.removeChildAt(i); } } // empty enemyA for(i=enemyA.length-1;i>=0;i– –){ enemyA[i].friendA.length=0; enemyA[i].foeA.length=0; EnemyTank_Pool.returnF(enemyA[i]); } enemyA.length = 0; if(player){ if(player.foeA){ player.foeA.length = 0; } PlayerTank_Pool.returnCombatF(player); } controller.dispatchEvent(new Event("cleanupE")); Controller_Pool.returnF(controller); } } } The CombatView_Pool supplies the CombatView instance (used IntroView to show how the player’s tank moves and shoots for combat and in while setting up the controls). CombatView_Pool package com.kglad{ public class CombatView_Pool { private static var pool:Vector.<CombatView>; public static function init(poolSize:int):void { pool = new Vector.<CombatView>(poolSize); for(var i:int=poolSize-1;i>=0;i– –){ pool[i] = new CombatView(); 279
280 Chapter 8 n Developing and Distributing Games for iOS Devices } } public static function retrieveF():CombatView { if (pool.length>0) { return pool.pop(); } else { // this branch should not execute. return new CombatView(); } } public static function returnF(view:CombatView):void { pool.push(view); } } } The Controller class handles user input and player tank actions. Controller package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.MouseEvent; import flash.display.Stage; import flash.utils.Timer; import flash.events.TimerEvent; import flash.media.Sound; import flash.utils.getTimer; import flash.sensors.Accelerometer; import flash.events.AccelerometerEvent; import flash.ui.Multitouch; import flash.ui.MultitouchInputMode; import flash.events.TouchEvent; public class Controller extends MovieClip{ private var player:PlayerTank; private var directionA:Array = []; private var enemyA:Array; private var tankA:Array; private var _stage:Stage; private var maxShells:int; private var maxHits:int; private var shellHit:Boolean; private var startTime:int; private var initTime:int;
The Tank Combat Game for the iPad protected const d2r:Number = Math.PI/180; protected var speed:int; protected var rotationRate:int; private var shellSpeed:int; private var shotSound:Sound = new ShotSound(); // I added this variable to prevent too many shot sounds // from occurring too close together in time. I noticed // there was a problem caused by a Flash maximum number // of concurrent sounds. private var lastShotTime:int; private var destroySound:Sound = new DestroySound(); private var keyCodeIndex:int; private var xLimit:int; private var yLimit:int; private var newDirectionFreq:int; private var newDirectionTimer:Timer; private var accelerometer:Accelerometer; // bv - boundary violation. tc - tank collision. used in // enemyMoveLimitsF private var bv:String; private var tc:String; private var enemyA_len:int; private var tankA_len:int; public function Controller() { accelerometer = new Accelerometer(); Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT; this.addEventListener("cleanupE",cleanupF,false,0,true); } public function init(_player:PlayerTank,_enemyA:Array):void{ // Controller is not added to the stage. So, I use // an init() method that is called from IntroView // and CombatView when those two class instances are // added to the stage. lastShotTime = 0; speed = Data.tankSpeed; shellSpeed = Data.shellSpeed; maxShells = Data.maxShells; maxHits = Data.maxHits; startTime = Data.startTime; speed = Data.tankSpeed; rotationRate = Data.rotationRate; newDirectionFreq = 10000/Data.enemyEvasiveness; accelerometer.addEventListener(AccelerometerEvent.UPDATE, controlChangePlayerF,false,0,true); 281
282 Chapter 8 n Developing and Distributing Games for iOS Devices player = _player; enemyA = _enemyA; enemyA_len = enemyA.length; xLimit = player.xLimit; yLimit = player.yLimit; tankA = enemyA.concat(player); tankA_len = tankA.length; _stage = player.stage; allTanksF(); enemyTankF(); playerTankF(); } private function allTanksF():void{ // initTime is used to ensure no tank starts firing // for first few seconds initTime = getTimer()+startTime*1000; } private function enemyTankF():void{ newDirectionF(); this.addEventListener(Event.ENTER_FRAME,directionChangeF,false,0,true); this.addEventListener(Event.ENTER_FRAME,rotateEnemyTurretF,false,0,true); this.addEventListener(Event.ENTER_FRAME,moveEnemyTankF,false,0,true); newDirectionTimer = new Timer(newDirectionFreq,0); newDirectionTimer.addEventListener(TimerEvent.TIMER,newDirectionF,false,0,true); // define for each enemy and offset start() if unsync’d // turning desired. newDirectionTimer.start(); for(var i:int=enemyA_len-1;i>=0;i– –){ // Each EnemyTank instance needs a different // Timer instance (moveBackTimer). When each // moveBackTimer calls its listener function, // I want to reference moveBackTimer’s // associated EnemyTank. But I have no way to // do that using the standard Timer instance. // So, I created a new class that extends and // adds one feature to the Timer class. I added // a scope property that I can set and get. enemyA[i].moveBackTimer = new TimerExt(1000,1); enemyA[i].moveBackTimer.addEventListener(TimerEvent.TIMER,moveBackF,false,0,true);
The Tank Combat Game for the iPad enemyA[i].moveBackTimer.scope = enemyA[i]; } } private function playerTankF():void{ player.score = 0; this.addEventListener(Event.ENTER_FRAME,movePlayerTankF,false,0,true); this.addEventListener(Event.ENTER_FRAME,rotatePlayerTurretF,false,0,true); // TouchEvent.TOUCH_TAP I found to be unreliable // whereas TouchEvent.TOUCH_BEGIN and // TouchEvent.TOUCH_END are reliable. // So, I added TouchEvent.TOUCH_END to arena_mc // to trigger player to shoot. And I decided to // allow the CustomCursor instance to be dragged // and dropped. player.parent.addEventListener(TouchEvent.TOUCH_BEGIN, startDragF,false,0,true); player.parent.addEventListener(TouchEvent.TOUCH_END, stopDragF,false,0,true); } /////////// begin all tanks ///////////////////// // tankCollisionF() replaces tank_v_tankCollisionsF and // is called from enemyMoveLimitsF()right after // boundaryViolationF(). Instead of destroying both tanks, // I handle tank collisions like boundary violations. // The tanks move back and then change direction. // For the most part this works well. private function tankCollisionF(tank:Tank):String{ for(var i:int=tankA_len-1;i>=1;i– –){ if(tank!=tankA[i] && tank.hitTestObject(tankA[i]) ) { return quadrantF(Tank(tank)); } } return ""; } // I use the same shootF for all EnemyTank instances and player protected function shootF(tank:Tank):void{ if(tank.shellA.length<maxShells && tank.foeA.length>0 && getTimer()>initTime){ // I added this to avoid exceeding the Flash // Player sound channel limit. There are other // ways to do this, but I liked this best because 283
284 Chapter 8 n Developing and Distributing Games for iOS Devices // there is no perceptible downside. if(getTimer()-lastShotTime>500){ shotSound.play(); lastShotTime = getTimer(); } var shell:Shell = new Shell(); tank.shellA.push(shell); if(tank.parent){ tank.parent.addChild(shell); } // I changed this to decrease the number of // calculations needed to update each shell’s // position. shell.vectorX = shellSpeed*Math.cos((tank.turret_mc.rotation+tank.rotation)*d2r); shell.vectorY = shellSpeed*Math.sin((tank.turret_mc.rotation+tank.rotation)*d2r); // shell’s initial position shell.tankX = tank.x; shell.tankY = tank.y; shell.x = tank.x+tank.gunL*shell.vectorX/ shellSpeed; shell.y = tank.y+tank.gunL*shell.vectorY/ shellSpeed; } // For EnemyTank instances, shootF is called in an // enterFrame loop so shellLoopF is called repeatedly // and updates the shell position of each EnemyTank. if((tank is EnemyTank) && tank.shellA.length>0){ shellLoopF(tank); } else if(player.shellA.length==1){ // For player, I need to add an enterFrame loop. this.addEventListener(Event.ENTER_FRAME,shellLoopF,false,0,true); } } private function shellLoopF(tankOrEvent):void{ // The passed parameter is either an EnemyTank instance // or an Event.ENTER_FRAME event. if(tankOrEvent is EnemyTank){ var tank:Tank = EnemyTank(tankOrEvent); } else { tank = player;
The Tank Combat Game for the iPad } var shellA:Array = tank.shellA; for(var i:int=shellA.length-1;i>=0;i– –){ shellHit = false; // The simplified calculation to determine each // shell’s position using the parameters defined // in shootF() shellA[i].x += shellA[i].vectorX; shellA[i].y += shellA[i].vectorY; // Check for hit vs foes hitF(tank.foeA,tank.shellA,i); // If a foe hit and this is player, display stat if(shellHit && tank.name == "player"){ PlayerTank(tank).score++; Data.score = PlayerTank(tank).score; // A scoreE event is dispatched to the // PlayerTank instance, which, in // CombatView, has a listener that // updates the display tank.dispatchEvent(new Event("scoreE")); } if(!shellHit){ // Check for hit vs friends hitF(tank.friendA,tank.shellA,i); } // If shell not removed because of a hit, // check for boundary violation. if(!shellHit){ if(boundaryViolationF(shellA[i])){ shellRemoveF(shellA,i); } } } } // friend/foe Array private function hitF(ffA:Array,shellA:Array,shellIndex:int):void{ for(var j:int=ffA.length-1;j>=0;j– –){ // Check for shell hit if(ffA[j].hitTestObject(shellA[shellIndex])){ // If a hit, assign shellHit shellHit = true; // Remove the shell shellRemoveF(shellA,shellIndex); // Update ffA[j]’s hits 285
286 Chapter 8 n Developing and Distributing Games for iOS Devices ffA[j].hits++; // If player hit, dispatch event so display // can be updated. if(ffA[j].name == "player"){ Data.playerHits = ffA[j].hits; // Dispatch a playerHitE event to // update the CombatView display. ffA[j].dispatchEvent(new Event("playerHitE")); } // Check if ffA[j] has reached maxHits limit if(ffA[j].hits>=maxHits){ // ffA[j] has been destroyed destroyF(ffA[j]); } break; } } } private function destroyF(tank:Tank):void{ destroySound.play(); // Add explosion. var explosion:Explosion = new Explosion(); // Listen for last frame to play in explosion (where a // "removeE" event is dispatched). explosion.addEventListener("removeE",removeExplosionF,false,0,true); tank.parent.addChild(explosion); // Position explosion where tank was located explosion.x = tank.x; explosion.y = tank.y; removeTankF(tank); } private function shellRemoveF(shellA:Array,i:int):void{ // I saw null object reference error here once during // testing, so I added this if-statement to prevent that. if(shellA[i].parent){ shellA[i].parent.removeChild(shellA[i]); } shellA.splice(i,1); if(player.shellA.length==0){ // Remove event listener if no longer needed. If // already removed, not a problem.
The Tank Combat Game for the iPad this.removeEventListener(Event.ENTER_FRAME,shellLoopF); } } private function moveLimitsF(tank:Tank):void{ if(boundaryViolationF(tank)){ tank.x = tank.prevX; tank.y = tank.prevY; } } protected function boundaryViolationF(mc:MovieClip):String{ var b:Boolean = false; if(mc.x<mc.width/2){ b=true; } else if(mc.x>xLimit-mc.width/2){ b=true; } else if(mc.y<mc.height/2){ b=true; } else if(mc.y>yLimit-mc.height/2){ b=true; } if(b){ if(mc is Tank){ return quadrantF(Tank(mc)); } else { return "true"; } } else { return ""; } } protected function quadrantF(tank:Tank):String{ if(!tank){ return ""; } else if(!tank.parent){ return ""; } var quadrant:String; if(tank.x<tank.parent.width/2){ quadrant="L"; } else { quadrant="R"; } if(tank.y<tank.parent.height/2){ 287
288 Chapter 8 n Developing and Distributing Games for iOS Devices quadrant+="U"; } else { quadrant+="D"; } return quadrant; } private function removeExplosionF(e:Event):void{ e.currentTarget.removeEventListener("removeE",removeExplosionF,false); if(e.currentTarget.stage){ e.currentTarget.parent.removeChild(e.currentTarget); } } // Destroyed tank (from Tank) is passed here so it can be // removed from all arrays: enemyA, player.foeA, all // enemyA member.foeA, and all enemyA.friendA private function removeTankF(tank:Tank):void{ // Remove if a player foe. if(player && tank.name!="player" && player.foeA){ for(var j2:int=player.foeA.length-1;j2>=0;j2– –){ if(player.foeA[j2].name==tank.name){ player.foeA.splice(j2,1); break; } } } // Remove from enemyA before check enemyA element foeA // and friendA. No need to look for tank in its own // foeA or enemyA. for(j2=enemyA.length-1;j2>=0;j2– –){ if(enemyA[j2].name==tank.name){ enemyA.splice(j2,1); break; } } for(j2=tankA.length-1;j2>=0;j2– –){ if(tankA[j2].name==tank.name){ tankA.splice(j2,1); break; } } for(j2=enemyA.length-1;j2>=0;j2– –){ // Remove if an enemy foe
The Tank Combat Game for the iPad for(var j3:int=enemyA[j2].foeA.length-1;j3>=0;j3–){ if(enemyA[j2].foeA[j3].name==tank.name){ enemyA[j2].foeA.splice(j3,1); break; } } // Remove if an enemy friend for(j3=enemyA[j2].friendA.length-1;j3>=0;j3– –){ if(enemyA[j2].friendA[j3].name==tank.name){ enemyA[j2].friendA.splice(j3,1); break; } } } if(tank.name=="player"){ player.parent.removeEventListener(TouchEvent. TOUCH_BEGIN, startDragF,false); player.parent.removeEventListener(TouchEvent. TOUCH_END, stopDragF,false); // PlayerTank instance is returned to PlayerTank_Pool // in CombatView } else { tank.friendA.length = 0; tank.foeA.length = 0; EnemyTank(tank).moveBackTimer.removeEventListener(TimerEvent.TIMER, moveBackF,false); EnemyTank_Pool.returnF(EnemyTank(tank)); } tank.parent.removeChild(tank); // Update array length variables. tankA_len = tankA.length; enemyA_len = enemyA.length; } /////////// end all tanks /////////////////////// /////////// begin player tank ////////////////// private function startDragF(e:TouchEvent):void{ if(player.cCursor){ player.cCursor.x = e.localX; player.cCursor.y = e.localY; 289
290 Chapter 8 n Developing and Distributing Games for iOS Devices // Again, the startTouchDrag and stopTouchDrag // methods need the touchPointID parameter. player.cCursor.startTouchDrag(e.touchPointID, false); } } private function stopDragF(e:TouchEvent):void{ if(player.cCursor){ player.cCursor.stopTouchDrag(e.touchPointID); // Here is where shootF is called by player when // arena_mc’s TouchEvent.TOUCH_END event is // dispatched. shootF(player); } } private function rotatePlayerTurretF(e:Event):void{ // if-statement used to prevent null object error. if(player.cCursor && player.parent){ player.turret_mc.rotation = -player.rotation+Math. atan2(player.cCursor.y-player.y,player.cCursor.x-player.x)/d2r; } } // This is the code to control player’s movement based on // the AccelerometerEvent dispatched by accelerometer. private function controlChangePlayerF(e:AccelerometerEvent):void{ // e.accelerationX, e.accelerationY, and e.accelerationZ // will change value as the iPad is rotated, pitched, // and yawed where I use those terms as if the iPad’s // screen were an airplane facing the user. In the // shorthand below, I use rotationX for the pitch of // the iPad, rotationY would be yaw but I don’t use it, // and rotationZ is the iPad’s rotation. If you have // trouble visualizing all that, create a test fla (if // you have Flash CS6) and test. If you don’t have Flash // CS6, you will need to use a textfield to see the // results of your iPad manipulations. // accelerationY: 1 -> 0 as rotationX: 0 -> -90 // It occurred to me that some people might want to go // forward when the iPad’s rotationX decreases (and the // pitch increases), whereas some might want to go // forward with the opposite rotation. // So, I have two if-branches for forward/back movement.
The Tank Combat Game for the iPad // One where Data.forward<Data.back and the other where // Data.forward>=Data.back. if(Data.forward<Data.back){ if(e.accelerationY<Data.forward){ directionA[0] = true; directionA[1] = false; } else if(e.accelerationY>Data.back){ directionA[0] = false; directionA[1] = true; } else { directionA[0] = false; directionA[1] = false } } else { if(e.accelerationY>Data.forward){ directionA[0] = true; directionA[1] = false; } else if(e.accelerationY<Data.back){ directionA[0] = false; directionA[1] = true; } else { directionA[0] = false; directionA[1] = false } } // accelerationX: 1 -> 0 -> -1 as rotationZ: // -90 -> 0 -> 90 // Data.right is negative, Data.left is positive // I didn’t think anyone would want to turn right // when they rotate left, so I didn’t add two // branches for left/right movement. if(e.accelerationX<Data.right){ directionA[2] = false; directionA[3] =true; } else if(e.accelerationX>Data.left){ directionA[2] = true; directionA[3] = false; } else { directionA[2] = false; directionA[3] = false; } } private function movePlayerTankF(e:Event):void{ if(directionA[0]){ 291
292 Chapter 8 n Developing and Distributing Games for iOS Devices player.x += speed*Math.cos(player.rotation*d2r); player.y += speed*Math.sin(player.rotation*d2r); } else if(directionA[1]){ player.x -= speed*Math.cos(player.rotation*d2r); player.y -= speed*Math.sin(player.rotation*d2r); } if(directionA[2]){ player.rotation -= rotationRate; } else if(directionA[3]){ player.rotation += rotationRate; } moveLimitsF(player); player.prevX = player.x; player.prevY = player.y; } //////////////////// end player tank ///////////////////// /////////////////// begin enemy tank ///////////////////// private function newDirectionF(e:TimerEvent=null):void{ for(var i:int=enemyA_len-1;i>=0;i– –){ var tank:EnemyTank = EnemyTank(enemyA[i]); if(!tank.moveBackBool){ // ai: turn towards stage center. var quadrant:String = quadrantF(tank); turnF(tank,quadrant); } } } private function rotateEnemyTurretF(e:Event):void{ for(var i:int=enemyA_len-1;i>=0;i– –){ var tank:EnemyTank = EnemyTank(enemyA[i]); // Shoot at closest foe. This is for // free-for-all mode. var closestFoe:Tank = closestFoeF(tank); // If there’s a foe, there’s a closest foe but // there may not be any foes remaining. if(closestFoe){ // Update turret rotation, then (try and) // shoot. tank.turret_mc.rotation = -tank.rotation+ Math.atan2(closestFoe.y-tank.y,closestFoe.x-tank.x)/d2r; shootF(tank); } else { for(var j:int=tank.shellA.length-1;j>=0;j– –){ shellRemoveF(tank.shellA,j);
The Tank Combat Game for the iPad } } } } private function closestFoeF(tank:EnemyTank):Tank{ var closestDist:Number = _stage.stageWidth* _stage.stageWidth+_stage.stageHeight*_stage.stageHeight; var closestFoe:Tank; for(var i:int=tank.foeA.length-1;i>=0;i– –){ var dist:Number = distF(tank,tank.foeA[i]); if(dist<closestDist){ closestDist = dist; closestFoe = tank.foeA[i]; } } return closestFoe; } private function distF(t:Tank,foeT:Tank):Number{ return (t.x-foeT.x)*(t.x-foeT.x)+(t.y-foeT.y)*(t.y-foeT.y); } private function moveEnemyTankF(e:Event):void{ for(var i:int=enemyA_len-1;i>=0;i– –){ var tank:EnemyTank = EnemyTank(enemyA[i]); if(tank.stage){ if(tank.startX==0){ tank.startX = this.x; tank.startY = this.y; } if(!tank.moveBackBool){ tank.rotation = tank.currentDirection; } tank.x = tank.startX+tank.distance*Math.cos(tank.currentDirection*d2r); tank.y = tank.startY+tank.distance*Math.sin(tank.currentDirection*d2r); tank.distance += speed; enemyMoveLimitsF(tank); tank.prevX = tank.x; tank.prevY = tank.y; } } 293
294 Chapter 8 n Developing and Distributing Games for iOS Devices } private function enemyMoveLimitsF(tank:EnemyTank):void{ bv = boundaryViolationF(tank); tc = tankCollisionF(tank); // If there is a boundary violation or tank v tank // collision, move backwards and assign nextDirection // to be towards stage-center. if(bv || tc){ tank.x = tank.prevX; tank.y = tank.prevY; // Move in opposite direction directionChangeUpdateF(tank); tank.currentDirection = (180+tank.currentDirection) %360; tank.moveBackBool = true; tank.moveBackTimer.start(); // assign nextDirection if(bv){ turnF(tank,bv); } else { turnF(tank,tc); } } } private function turnF(tank,quadrant:String):void{ if(quadrant=="LU"){ tank.nextDirection = int(90*Math.random()); } else if(quadrant=="LD"){ tank.nextDirection = 270+int(90*Math.random()); } else if(quadrant=="RU"){ tank.nextDirection = 90+int(90*Math.random()); } else { tank.nextDirection = 180+int(90*Math.random()); } } private function moveBackF(e:TimerEvent):void{ var tank:EnemyTank = EnemyTank(e.target.scope); directionChangeUpdateF(tank); tank.moveBackBool = false; tank.currentDirection = (180+tank.currentDirection)%360; } private function directionChangeF(e:Event):void{ for(var i:int=enemyA_len-1;i>=0;i– –){ var tank:EnemyTank = EnemyTank(enemyA[i]);
The Tank Combat Game for the iPad if(!tank.moveBackBool && Math.abs(tank. currentDirection-tank.nextDirection)>rotationRate/2){ // transition from currentDirection to // nextDirection the "short" way. if(tank.currentDirection<=tank. nextDirection){ if(tank.nextDirection-tank. currentDirection<=180){ // increment currentDirection tank.currentDirection +=rotationRate; } else { // decrement currentDirection. // (eg, currentDirection=5 & // nextDirection=355, go the // "short" way, 5 to 355 tank.currentDirection =(tank. currentDirection-rotationRate+360)%360; } } else { // currentDirection>nextDirection if(tank.currentDirectiontank.nextDirection<180){ // decrement currentDirection tank.currentDirection=rotationRate; } else { // increment currentDirection tank.currentDirection = (tank. currentDirection+rotationRate)%360; } } directionChangeUpdateF(tank); } } } private function directionChangeUpdateF(tank:EnemyTank):void{ tank.distance = speed; 295
296 Chapter 8 n Developing and Distributing Games for iOS Devices tank.startX = tank.x; tank.startY = tank.y; } /////////////////// end enemy tank /////////////////////// private function cleanupF(e:Event):void{ this.removeEventListener(Event.ENTER_FRAME,shellLoopF, false); this.removeEventListener(Event.ENTER_FRAME,rotatePlayerTurretF,false); this.removeEventListener(Event.ENTER_FRAME,directionChangeF,false); this.removeEventListener(Event.ENTER_FRAME,rotateEnemyTurretF,false); this.removeEventListener(Event.ENTER_FRAME,moveEnemyTankF, false); this.removeEventListener(Event.ENTER_FRAME, movePlayerTankF,false); accelerometer.removeEventListener(AccelerometerEvent. UPDATE, controlChangePlayerF,false); if(player.parent){ player.parent.removeEventListener(TouchEvent. TOUCH_BEGIN, startDragF,false); player.parent.removeEventListener(TouchEvent. TOUCH_END, stopDragF,false); player.parent.removeChild(player); } this.removeEventListener(Event.ENTER_FRAME,shellLoopF); for(var i:int=enemyA.length-1;i>=0;i– –){ enemyA[i].moveBackTimer.stop(); enemyA[i].moveBackTimer.removeEventListener(TimerEvent.TIMER,moveBackF,false); if(enemyA[i].stage){ enemyA[i].parent.removeChild(enemyA[i]); } } newDirectionTimer.stop(); newDirectionTimer.removeEventListener(TimerEvent.TIMER,newDirectionF,false); enemyA.length = 0; tankA.length = 0; } } } The Controller_Pool class suppies the Controller instance.
The Tank Combat Game for the iPad Controller_Pool package com.kglad{ public class Controller_Pool { private static var pool:Vector.<Controller>; public static function init(poolSize:int):void { pool = new Vector.<Controller>(poolSize); pool[0] = new Controller(); } public static function retrieveF():Controller { if (pool.length>0) { return pool.pop(); } else { // this branch should not execute. return new Controller(); } } public static function returnF(controller:Controller):void { pool.push(controller); } } } I needed to extend the Timer class with a Timer that could track its scope. TimerExt package com.kglad{ import flash.utils.Timer; import flash.display.MovieClip; public class TimerExt extends Timer { private var _scope:MovieClip public function TimerExt(delay:Number, repeatCount:int = 0){ super(delay,repeatCount); } public function set scope(mc:MovieClip):void{ _scope = mc; } public function get scope():MovieClip{ return _scope; } } } The base Tank class that PlayerTank and EnemyTank extend, follows. 297
298 Chapter 8 n Developing and Distributing Games for iOS Devices Tank package com.kglad { // All the code to control the tanks is in the Controller class. // That change did not enhance encapsulation, but it did increase // efficiency because I was able to eliminate enterFrame listeners // added to each tank. import flash.display.MovieClip; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Matrix; public class Tank extends MovieClip{ internal var xLimit:int; internal var yLimit:int; internal var hits:int; internal var gunL:Number; internal var prevX:Number; internal var prevY:Number; internal var shellA:Array = []; // array of foes internal var foeA:Array = []; // array of friends internal var friendA:Array = []; public function Tank(_xLimit:int,_yLimit:int) { xLimit = _xLimit; yLimit = _yLimit; gunL = this.turret_mc.width-this.turret_mc.top_mc.width/2; // I cached the turrets but didn’t feel caching the tanks // would be helpful because the tank bitmap would need to // be re-cached with every turret rotation. this.turret_mc.cacheAsBitmap = true; this.turret_mc.cacheAsBitmapMatrix = new Matrix(); // None of the tanks or their children need to interact // with the mouse. this.mouseEnabled = false; this.mouseChildren = false; this.addEventListener(Event.ADDED_TO_STAGE,init,false,0, true); this.addEventListener(Event.REMOVED_FROM_STAGE, removedF,false,0,true); }
The Tank Combat Game for the iPad private function init(e:Event):void{ // initialize hits hits = 0; } private function removedF(e:Event):void{ for(var i:int=shellA.length-1;i>=0;i– –){ if(shellA[i].parent){ shellA[i].parent.removeChild(shellA[i]); } } shellA.length = 0; } } } The PlayerTank class handles the custom cursor. PlayerTank package com.kglad { import flash.display.MovieClip; import flash.events.Event; public class PlayerTank extends Tank { internal var cCursor:CustomCursor; // Number of foes shot. internal var score:int; public function PlayerTank(_xLimit:int,_yLimit:int) { super(_xLimit,_yLimit); cCursor = new CustomCursor(); this.addEventListener(Event.ADDED_TO_STAGE,init,false,0, true); this.addEventListener(Event.REMOVED_FROM_STAGE,removedP, false,0,true); } private function init(e:Event):void{ addCustomCursorF(); } private function addCustomCursorF():void{ // There’s no mouse to hide and show. The only thing // this class controls is adding and removing the // CustomCursor. this.parent.addChild(cCursor); } 299
300 Chapter 8 n Developing and Distributing Games for iOS Devices private function removeCustomCursorF():void{ this.parent.removeChild(cCursor); } private function removedP(e:Event):void{ if(cCursor.stage){ removeCustomCursorF(); } } } } The PlayerTank_Pool class supplies two player tank instances: one for one for CombatView. IntroView and PlayerTank_Pool package com.kglad{ // This class must pool two different PlayerTanks, one for // IntroView and one for CombatView, because of the different // boundaries in those two views. public class PlayerTank_Pool { private static var xLimitIntro:int; private static var yLimitIntro:int; private static var xLimitCombat:int; private static var yLimitCombat:int; // Two vector pools. private static var introPool:Vector.<PlayerTank>; private static var combatPool:Vector.<PlayerTank>; public static function init(_xLimitIntro:int,_yLimitIntro:int,_xLimitCombat:int,_yLimitCombat:int):void { // Assign the boundary limits for both views. xLimitIntro = _xLimitIntro; yLimitIntro = _yLimitIntro; xLimitCombat = _xLimitCombat; yLimitCombat = _yLimitCombat; introPool = new Vector.<PlayerTank>(1); combatPool = new Vector.<PlayerTank>(1); // Populate the two vectors with the appropriate PlayerTank introPool[0] = new PlayerTank(xLimitIntro,yLimitIntro); combatPool[0] = new PlayerTank(xLimitCombat,yLimitCombat); } // Two different retrieve and return functions, but otherwise // this is a typical pool class. public static function retrieveIntroF():PlayerTank { if (introPool.length>0) {
The Tank Combat Game for the iPad return introPool.pop(); } else { // this branch should not execute. return new PlayerTank(xLimitIntro,yLimitIntro); } } public static function returnIntroF(tank:PlayerTank):void { introPool.push(tank); } public static function retrieveCombatF():PlayerTank { if (combatPool.length>0) { return combatPool.pop(); } else { // this branch should not execute. return new PlayerTank(xLimitCombat,yLimitCombat); } } public static function returnCombatF(tank:PlayerTank):void { combatPool.push(tank); } } } The EnemyTank class handles the color for the enemy tanks. EnemyTank package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.geom.ColorTransform; import flash.utils.Timer; public class EnemyTank extends Tank { internal var moveBackBool:Boolean; internal var moveBackTimer:Timer; internal var currentDirection:int; internal var nextDirection:int; internal var distance:int; internal var startX:int; internal var startY:int; public function EnemyTank(_xLimit:int,_yLimit:int) { super(_xLimit,_yLimit); // colorF() is the only thing left in EnemyTank // besides the variable declarations. 301
302 Chapter 8 n Developing and Distributing Games for iOS Devices colorF(); } private function colorF():void{ var ct:ColorTransform = new ColorTransform(); ct.color = 0x990000; this.base_mc.transform.colorTransform = ct; ct.color = 0x660000; this.turret_mc.top_mc.transform.colorTransform = ct; } } } The EnemyTank_Pool class supplies up to 20 enemy tanks. EnemyTank_Pool package com.kglad{ public class EnemyTank_Pool { private static var xLimit:int; private static var yLimit:int; private static var pool:Vector.<EnemyTank>; public static function init(poolSize:int,_xLimit:int,_yLimit:int): void { xLimit = _xLimit; yLimit = _yLimit; pool = new Vector.<EnemyTank>(poolSize); for(var i:int=poolSize-1;i>=0;i– –){ pool[i] = new EnemyTank(xLimit,yLimit); } } public static function retrieveF():EnemyTank { if (pool.length>0) { return pool.pop(); } else { // this branch should not execute. return new EnemyTank(xLimit,yLimit) } } public static function returnF(tank:EnemyTank):void { pool.push(tank); } } } The GameOverView class handles the view presented after a combat round completes.
The Tank Combat Game for the iPad GameOverView package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.TouchEvent; import flash.text.TextField; import flash.text.TextFormat; import flash.text.Font; import flash.utils.getTimer; import flash.ui.Multitouch; import flash.ui.MultitouchInputMode; public class GameOverView extends MovieClip { var tf:TextField; public function GameOverView() { // Code that doesn’t need to be re-executed when this // instance is added to the stage has been moved to the // constructor. Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT; this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanupF,false,0, true); var tfor:TextFormat = new TextFormat(); var verdana:Verdana = new Verdana(); tfor.font = verdana.fontName; tfor.size = 14; tf= new TextField(); tf.embedFonts = true; // Use defaultTextFormat when assigning a TextFormat // instance before text is assigned. After text is // assigned, use the setTextFormat() method. tf.defaultTextFormat = tfor; tf.multiline = true; tf.width = 400; tf.autoSize = "left"; tf.y = 200; } private function init(e:Event):void{ listenersF(); if(Data.enemiesRemaining==0){ var s:String = "You won!!\n\nYou had "; } else { 303
304 Chapter 8 n Developing and Distributing Games for iOS Devices s = "You lost!\n\nYou had "; } s+= Data.score+" enemy hits\nand you were shot "+Data. playerHits+" times.\n\nThere were "+Data.enemiesRemaining+" enemies remaining."; addChild(tf); tf.x = (stage.stageWidth-tf.width)/2; tf.text = s; } private function listenersF():void{ replay_mc.addEventListener(TouchEvent.TOUCH_TAP,replayF,false,0,true); replay_mc.buttonMode = true; } private function cleanupF(e:Event):void{ replay_mc.removeEventListener(TouchEvent.TOUCH_TAP, replayF,false); } private function replayF(e:Event):void{ dispatchEvent(new Event("replayE")); } } } The GameOverView_Pool supplies the one GameOverView instance. GameOverView_Pool package com.kglad{ public class GameOverView_Pool { private static var pool:Vector.<GameOverView>; public static function init(poolSize:int):void { pool = new Vector.<GameOverView>(poolSize); for(var i:int=poolSize-1;i>=0; i– –){ pool[i] = new GameOverView(); } } public static function retrieveF():GameOverView { if (pool.length>0) { return pool.pop(); } else { // this branch should not execute. return new GameOverView(); } }
The Tank Combat Game for the iPad public static function returnF(view:GameOverView):void { pool.push(view); } } } The Data class gets and sets the parameters that customize the game and the values presented by GameOverView. Data package com.kglad { public class Data { // variables with default values below // The only changes in this class are the changes needed // to use accelerometerEvent properties for _forward, // _back, _left, _right private static var _forward:Number ; private static var _back:Number; private static var _left:Number; private static var _right:Number; private static var _freeForAll:Boolean; private static var _tankSpeed:int; private static var _rotationRate:int = 5; private static var _shellSpeed:int; private static var _maxShells:int private static var _enemyNum:int; private static var _maxHits:int; private static var _enemyEvasiveness:int; private static var _startTime:int private static var _combatEndDelay:int; // Default values for above variables. Assigned in // resetAllF() below private static var defaultValueA:Array = [false,.5,.7,-.1,.1,4,10,1, 5,1,3,3,3]; public static var defaultVariableA:Array = ["_freeForAll","_forward","_back","_left","_right","_tankSpeed","_shellSpeed", "_maxShells","_maxHits","_enemyNum","_enemyEvasiveness","_startTime","_combatEndDelay"]; private static var _playerHits:int; private static var _score:int; private static var _enemiesRemaining:int; public function Data() { // constructor code } public static function set forward(n:Number):void{ 305
306 Chapter 8 n Developing and Distributing Games for iOS Devices _forward = n; } public static function set back(n:Number):void{ _back = n } public static function set left(n:Number):void{ _left = n; } public static function set right(n:Number):void{ _right = n } public static function get forward():Number{ return _forward; } public static function get back():Number{ return _back } public static function get left():Number{ return _left; } public static function get right():Number{ return _right; } public static function set playerHits(n:int):void{ _playerHits = n; } public static function get playerHits():int{ return _playerHits; } public static function set score(n:int):void{ _score = n; } public static function get score():int{ return _score; } public static function set enemiesRemaining(n:int):void{ _enemiesRemaining = n; } public static function get enemiesRemaining():int{ return _enemiesRemaining; } public static function set freeForAll(b:Boolean):void{ _freeForAll = b; }
The Tank Combat Game for the iPad public static function get freeForAll():Boolean{ return _freeForAll; } public static function set tankSpeed(n:int):void{ _tankSpeed = n; } public static function get tankSpeed():int{ return _tankSpeed; } public static function get rotationRate():int{ return _rotationRate; } public static function set shellSpeed(n:int):void{ _shellSpeed = n; } public static function get shellSpeed():int{ return _shellSpeed; } public static function set maxShells(n:int):void{ _maxShells = n; } public static function get maxShells():int{ return _maxShells; } public static function set maxHits(n:int):void{ _maxHits = n; } public static function get maxHits():int{ return _maxHits; } public static function set enemyNum(n:int):void{ _enemyNum = n; } public static function get enemyNum():int{ return _enemyNum; } public static function set enemyEvasiveness(n:int):void{ _enemyEvasiveness = n; } public static function get enemyEvasiveness():int{ return _enemyEvasiveness; } public static function set startTime(n:int):void{ _startTime = n; 307
308 Chapter 8 n Developing and Distributing Games for iOS Devices } public static function get startTime():int{ return _startTime; } public static function set combatEndDelay(n:int):void{ _combatEndDelay = n; } public static function get combatEndDelay():int{ return _combatEndDelay; } public static function resetAllF():void{ for(var i:int=defaultValueA.length-1;i>=0;i– –){ Data[defaultVariableA[i]] = defaultValueA[i]; } } } } Testing an iOS Game At the time of this writing, there is no iOS emulator. However, Flash Pro CS6 has a worthwhile mobile simulator you can use to simulate certain aspects of the mobile (including iOS and Android devices) experience. Specifically, the simulator allows you to test touches, gestures, acceleration of the device itself (almost as if you were holding it and rotating and tilting it), and geolocation. It is far from ideal, mainly because it doesn’t emulate iOS performance, but it is still very useful for testing and is a major timesaver. Also, you cannot test (or, at least, it is difficult to test) more than one of the three mobile-specific features (Accelerometer, Touch and Gesture, and Geolocation) at any one time. Hopefully, Adobe will expand the simulator’s capabilities to simulate mobile devices in future releases of Flash Pro. (That feature request has been logged.) To use the simulator, in Flash Pro CS6, click Control > Test Movie > In Air Debug Launcher (Mobile). You should see two panels open. n The Simulator Controller panel (see Figure 8.1)
Testing an iOS Game The Accelerometer menu The Touch and Gesture menu The Geolocation menu Figure 8.1 The mobile Simulator panel with three accordion menus. Source: Adobe Systems Incorporated. 309
310 Chapter 8 n Developing and Distributing Games for iOS Devices n The game in an Air display (see Figure 8.2) Figure 8.2 Tank combat in the Air Debug Launcher. Source: Adobe Systems Incorporated. When the Simulator panel opens, the Accelerometer menu is expanded. If you play with the X-axis, Y-axis, and Z-axis sliders, you can see the tank moving and responding to the accelerometer. But actually making the tank do what you want is close to impossible. We’ll fix that. First, reset the simulated device’s position by clicking the Reset icon (see Figure 8.3).
Testing an iOS Game The Accelerometer menu’s settings icon The Accelerometer menu’s reset icon Figure 8.3 Clicking the Accelerometer menu’s Reset icon reorients the simulated device’s position. Source: Adobe Systems Incorporated. Then drag the thumb of the X-slider to rotate the device about the X-axis, setting the X-rotation to about −50 degrees. (Alternatively, you can enter −50 into the input field to the right of the X-slider and press the Enter key.) Click to expand the Touch and Gesture menu and check Click and Drag (if it’s not already checked). You will now be able to simulate a touch event on the game’s Air display. As you mouse over the Air display, you will see your mouse pointer change to indicate that it will simulate a touch event when you click. Click the Forward button in the Air display. You have now defined the X-rotation needed to move the tank forward. Now define the X-rotation needed to move the tank back. Expand the Accelerometer menu and change the X-rotation to about −20 degrees. Expand the Touch and Gesture menu and then click the Back button in the Air 311
312 Chapter 8 n Developing and Distributing Games for iOS Devices display. Now, unless your tank is in a corner, you should be able to expand the Accelerometer menu and drag the X-axis slider to the left and right, making the tank go forward, stop, and go back, and feel as if you have fair control of the tank. With the tank stopped (in other words, set X-rotation to between −50 and −20 degrees), use the Z-slider to customize the tank’s left and right rotations. Drag the Z-slider to the right to about 30 degrees, expand the Touch and Gesture menu, and then click the left button in the Air display. You have defined the Z-rotation needed to rotate your tank left. Likewise, to customize the rotate tank right feature, expand the Accelerometer menu, drag the Z-slider to about −30 degrees, expand the Touch and Gesture menu, and click the right button in the Air display. It isn’t very convenient to go back and forth among the three menus, but often it’s more convenient than going back and forth between your development platform and an iPad or other device. Playing the game using the simulator is impossible because you must use the accelerometer and touch simulations at the same time, and that’s not possible using the Flash Air Debug Launcher (Flash ADL). Once you have finished debugging any major part of your game using the Flash ADL, you should test on the target platform and debug any problems you see when testing on the deployment platform. Hopefully, you will have debugged most of the problems using the Flash ADL so you won’t need to repeatedly publish your game, add your game to your target device, and then test on your target device. That sequence of steps is very time-consuming. I just want to mention one more tip about the Simulator Controller. When using the Accelerometer menu, if you want the Air display to reflect a vertical and horizontal device orientation as you change the Z-rotation in the Simulator Controller, you must check the Auto Orientation option in the Settings subpanel. You open that subpanel by clicking the Settings icon (to the right of the Reset icon). Refer to Figure 8.3. The next section will cover how to publish an .ipa file, add it to your iTunes, and load it onto a target device. Publishing Your Game for iOS To publish a game you are developing for iOS, you will need: n A development Certificate (a secure certificate that allows you to develop iOS apps/games) n For each app/game, a development Provisioning Profile
Publishing Your Game for iOS Using both of those, you can publish apps/games that can be tested—but only on devices you designate to the authorities at Apple and only after you pay $99 per year (for your Developer License) and navigate an overly complicated process to obtain those two files. Then, if you want to submit an app/game to the Apple Store (or you want to distribute your game to users with unknown devices), you must also obtain: n A distribution Certificate (a secure certificate that allows you to distribute iOS apps/games) n For each app/game, a distribution Provisioning Profile You must complete the following steps to obtain the first two files that are required to develop an iOS game. This list is not intended to be casually read. Dog-ear or otherwise note this page and use it as a reference to obtain the files you need when you need them. I envision you and me, the next time I need one of the above files, navigating to the Apple Developer page and following the relevant steps for the needed file(s). You won’t need to follow all 29 steps every time you want to create a new app/game. Some steps (1–13) only need to be followed once per year (unless you develop more than 100 apps/games in less than a year). And some steps (14–16) are also less frequently needed than the main steps (17–29), which you must follow each time you want to develop a new app/game. n You need to repeat the first 13 steps only once each year, when your Apple Developer License expires and you need another Certificate file. You will need to use a Mac or virtual Mac computer (Mac OS running on a PC) for some of these steps. n You will need to follow Steps 14, 15, and 16 to add a new device for testing. You can do that anytime your license is valid. You only need to use a Mac or virtural Mac computer if you need to determine a device’s UDID (Unique Device Identifier). n You need to follow Steps 17 to 29 each time you want to develop an additional app/game with a new App ID. You can use a PC for all these steps. 1. Go to https://developer.apple.com/devcenter/ios/index.action using a Mac or a virtual Mac. 2. Log in or, if you aren’t registered, register and then log in. (Registration costs $99/year.) 313
314 Chapter 8 n Developing and Distributing Games for iOS Devices 3. Click on the iOS Dev Center link. 4. Click on the iOS Provisioning Portal link. (See Figure 8.4.) The Request Certificate button to start the process of obtaining your development certificate Figure 8.4 iOS Provisioning Portal page showing left panel links Home, Certficates, Devices, App IDs, Provisioning, and Distribution. You will need to use all but Home and Distribution. Source: Apple® Inc. 5. Click on the Certificates link in the left panel. 6. Click on the Development tab in the right panel. 7. Click on the Request Certificate button in the right panel. 8. Follow the directions using Keychain Access to create your development Certificate. (See Figure 8.5.) Apple has easy-to-follow directions to create a Certificate Signing Request that is used to create your Certificate. The steps needed to create a Certificate Signing Request appear in the right panel after Step 7. The only part I stumble on is when instructed to open Certificate Assistant. You will find it at the top left of your Mac’s screen after clicking Keychain Access, not in the Keychain Access window.
Publishing Your Game for iOS Figure 8.5 After clicking Request Certificate, you’ll see this helpful page of instructions. Source: Apple® Inc. 9. The right panel should change to show that your Certificate is pending issuance. Refresh the page, and you should see that your Certificate is issued and ready for download. (See Figure 8.6.) 315
316 Chapter 8 n Developing and Distributing Games for iOS Devices Figure 8.6 After following the directions for using Keychain Access and uploading your Certificate Signing Request, you should see this page with Download replaced by Pending. Refresh the page to see the Download button. Source: Apple® Inc. 10. Download your ios_development.cer by clicking the Download button. 11. Download an AppleWWDRCA.cer by clicking the *If You Do Not Have the WWDR Intermediate Certificate Installed, Click Here to Download Now link. 12. Open both files in Keychain Access (using the login Keychain). 13. Right-click your private key in Keychain Access (you may need to expand the iPhone Developer certificate shown in your Keychain Access window) and click Export. Make sure File Format is Personal Information Exchange (.p12) and click Save. You will be prompted for a password that you must remember. You will use that password each and every time you publish an iOS game, and you will use your .p12 (development Certificate) file for all the apps/games you develop (until your Certificate expires and you pay another $99 and follow these steps again). Congratulations, you have completed the steps needed to obtain the first of the four files mentioned previously! 14. Click on the Devices link in the left panel. Then click the Add Devices button toward the upper right to add devices that will be used to test your game. You
Publishing Your Game for iOS usually cannot test your game on a device unless you add it here or your device is jailbroken. There is an exception (using a distribution Certificate and ad hoc distribution Provisioning Profile) covered in the next section. (See Figure 8.7.) Figure 8.7 After clicking Devices, you should see this. Source: Apple® Inc. 15. You will need to enter the device name (pick something sensible) and the device ID. The device ID is the UDID that will be used for testing the app/game. To find a device’s UDID, follow the instructions in Steps 16a, 16b, 16c, and 16d. Otherwise, you can skip those steps. (See Figure 8.8.) 317
318 Chapter 8 n Developing and Distributing Games for iOS Devices Figure 8.8 After clicking the Add Devices button, you should see this, where you enter an app name and UDID and click the plus sign to enter more names and UDIDs or click Submit. Source: Apple® Inc. 16a. Open iTunes and connect your device to your Mac, virtual Mac, or PC with iTunes installed. 16b. In the left panel of iTunes, find your device and click on it to reveal summary info about it in the right panel. (Make sure the Summary tab is selected to see that info.) 16c. Click the device’s serial number (in the right panel) to reveal the device’s identifier. That identifier is the UDID. Copy it to your clipboard and paste it. 16d. Click the plus sign to add more devices or repeatedly click Add Devices. (You are limited to 100 testing devices per Certificate.) 17. Click the App IDs link in the left panel. 18. Click the New App ID button (toward the upper right). (See Figure 8.9.)
Publishing Your Game for iOS Figure 8.9 After clicking App IDs on the left and New App ID on the right, you should see this, where you enter a description, Bundle Seed ID, and Bundle Identifier. Source: Apple® Inc. 19. Enter your App description (using only alphanumeric characters). 20. Select Team ID (unless you already have a Bundle Seed ID that you wish to reuse for related apps/games) in the Bundle Seed ID combobox. 21. Enter your Bundle Identifier. If you have a website, use com.yourwebsitename. yourappname for your Bundle Identifier. 22. Click Submit. 23. You should be back at the main App IDs link, and your new App ID (along with all of your other App IDs) should be listed. (See Figure 8.10.) 319
320 Chapter 8 n Developing and Distributing Games for iOS Devices Figure 8.10 You should see this after submitting your new App ID. Source: Apple® Inc.
Publishing Your Game for iOS 24. Click the Configure link if you want to add Apple Push Notification service and follow that multistep procedure, which has helpful instructions. 25. Click Provisioning in the left panel and, in the right panel, confirm that the Distribution tab is selected and click the New Profile button toward the upper right. (See Figure 8.11.) Figure 8.11 You should see this after clicking New Profile. Source: Apple® Inc. 26. Enter a Provisioning Profile name, check whose Certificate will be used with the Provisioning Profile, select the desired App ID from the combobox, check the device that will be used for testing during development, and click Submit. 27. You should be back at the Provisioning link/Development tab, and the status (fourth column) of your new profile is probably pending. (Mine always is.) See Figure 8.12, where I just added a “switcher” Profile. 321
322 Chapter 8 n Developing and Distributing Games for iOS Devices Figure 8.12 You should see this after submitting a new developer Provisioning Profile. Source: Apple® Inc. 28. Refresh the page. Your Provisioning Profile’s status should change to active, and a Download button should appear. If it doesn’t, go have a frappe and refresh the page when you return. (See Figure 8.13.)
Publishing Your Game for iOS Figure 8.13 You should see this after refreshing the Provisioning page. Source: Apple® Inc. 29. Click the Download button to download your Provisioning Profile. Save this to the directory where you are developing your app/game. Each app/game will have its own Provisioning Profile. With your development certificate and development Provisioning Profile, you’re ready to publish an iOS game from Flash Pro. After exploring how to publish an iOS game using your .p12 development Certificate and your development Provisioning Profile, I will list the steps needed for you to obtain a distribution Certificate and distribution Provisioning Profile. Those steps are similar to the steps just listed. Air for iOS Settings: General Tab Open Flash and load the FLA that corresponds to the Provisioning Profile you just created or create a new FLA that will start the game that corresponds to your Provisioning Profile. 323
324 Chapter 8 n Developing and Distributing Games for iOS Devices Click File > Publish Settings > SWF and select the most recent version of Air for iOS in the Target combobox (see Figure 8.14). To the right of the target combobox is the Player Settings icon. Click it to open the Player Settings panel. Figure 8.14 After clicking File > Publish Settings, you should see the Publish Setting panel. Source: Adobe Systems Incorporated. To the right of the Target combo box is the Player Settings icon. Click it to open the Player Settings panel. If you selected Air for iOS in the Target combo box this panel should be the Air for iOS Settings panel (see Figure 8.15).
Publishing Your Game for iOS Figure 8.15 The Air for iOS Settings panel, General tab. Source: Adobe Systems Incorporated. Click the General tab if it isn’t already selected. You should see several fields for text and several comboboxes. The Output File field contains the name of the .ipa file, which Flash will publish. That file will be installed on the device for testing. The App Name field holds the name of your game, which will appear under your game’s icon when installed on an iOS device (and in iTunes). A limited number of characters will display. 325
326 Chapter 8 n Developing and Distributing Games for iOS Devices The Version value doesn’t need to change. That will only become important when you publish an .ipa file for distribution. We’re still in the development phase. The Aspect Ratio field should be Auto if you want your game to rotate when the device is rotated. Otherwise, select Portrait or Landscape. Check Full Screen and/or Auto orientation if desired. Auto orientation should be checked if you selected Auto in the Aspect Ratio combobox. Render Mode is probably the most important option in this panel. What you select here can make the difference between a poorly performing game and a game with smooth play. There are some guidelines for what you should select, but none of them is worthwhile except that if you’re using the Stage3D API, you must select Direct. Otherwise, you should test with both CPU and GPU to see which is best. Generally, if you’re doing bitmap manipulation, GPU will usually be better. But I don’t think that’s helpful. You won’t care whether performance is usually better with GPU Render mode. You want to know if performance will be better with GPU mode. And that is best determined by testing the performance with CPU and then testing with GPU. Again, if you’re using Stage3D, choose Render mode Direct. You have no other option that will work with Stage3D. If you are not using Stage3D, choose Render mode CPU and test on your target iOS device. Then choose Render mode GPU and test on your target iOS device. Use the Render mode that worked best when you tested. There is also an Auto Render mode, but I believe that’s still the same as CPU mode. Eventually, some sort of Auto Render mode may be implemented by Adobe (or abandoned), but as of the time of this writing, Auto and CPU mode are the same. What you select for Device should be obvious. Select iPhone and/or iPad depending on your target platform. Select High for Resolution if you’re targeting devices with retina displays and your game’s performance is satisfactory. Otherwise, select Standard. And lastly, the Included Files section of this panel should contain two files by default: the published SWF and an XML file that Adobe calls your application descriptor. The descriptor contains the information from the Air for iOS Settings panel that you’re currently editing. In addition, you can (and should) add any files needed by your game. You don’t need to add any of your class files because all that code is compiled into your SWF by
Publishing Your Game for iOS Flash when you publish the SWF. But if you load bitmaps or data files or another SWF, they should be added to this section. Also, if you load another SWF and it contains ActionScript, none of the code will execute. That’s an Apple restriction. So, you are limited to loading SWFs that contain graphic assets that can be used in your main SWF. You can click the icon on the left to add individual files, and you can click the icon on the right to add entire directories of files. The middle icon is used to remove something you previously added that is no longer needed. Air for iOS Settings: Deployment Tab Click the Deployment tab (see Figure 8.16). Figure 8.16 Air for iOS Settings panel with Deployment tab selected. Source: Adobe Systems Incorporated. 327
328 Chapter 8 n Developing and Distributing Games for iOS Devices To the right of the Certificate field is a Browse button that you should click to navigate to your development Certificate .p12 file. Click the file and click Open. Enter the Certificate password you created in Keychain Access and check Remember Password for This Session if you do not wish to repeatedly enter your password each time you publish your game. To the right of the Provisioning Profile field is another Browse button that you should click to navigate to your development Provisioning Profile .mobileprovision file. Click the file and click Open. Alert: What follows is a little screwy. In the App ID field, enter the Bundle Identifier (in other words, App ID suffix) you used in Step 21. If you click the App ID or Provisioning link at the iOS Provisioning Portal, you will see a Bundle Seed ID (an alphanumeric string) dot followed by your Bundle Identifier. That concatenated string is what Apple displays as the App ID. But, for publishing your iOS game, you only want to use the suffix of that concatenated string. For example, the Provisioning link at the Provisioning Portal, for my switcher game, shows 547746GW93.switches (refer to Figure 8.12). The App ID to be used in Flash is switches. (The fact that switches and switcher are not identical was an inadvertent mixup on my part.) Finally, select one of the following iOS deployment option types. 1. Quick publishing for device testing 2. Quick publishing for device debugging 3. Deployment - Ad Hoc 4. Deployment - Apple App Store Option 1 Use Option 1 for testing on an iOS device. This is the fastest option and will publish an .ipa file. Add that .ipa file to your Mac or virtual Mac iTunes Apps and connect your iOS device to your Mac or virtual Mac. iTunes should detect your device and list it under Devices. Click your device and click Apps. You should see your app/game listed. Click it to select it and click Sync to load the app/game to your device. If you did everything correctly, your app/game will load without a problem. If it loads, go to your device and test your game. Otherwise, you will see a more-or-less cryptic error message. If you see an error message, either retrace the steps, especially checking the App ID you entered into the Air for iOS Settings panel, or copy the error message and search for help using Google.
Publishing Your Game for iOS Option 2 If you want to test on an iOS device and see trace output in Flash, select Option 2. You should see a network interface for remote debugging displayed in the combobox. Copy the IP address if that’s what you’re going to select. After selecting a network interface, publish your .ipa file. Follow the same steps listed in Option 1 to load your game onto your iOS device. In Flash, click Debug > Begin Remote Debug Session > ActionScript 3.0. Tap your newly loaded game. It may take a minute to start up. When it does, you should see a Flash Debugger prompt for the IP address or hostname you just copied. Type the IP address or hostname and tap OK. If all goes well, you will see trace output from your device using your network connection. If you cannot connect, make sure your iOS device has access to the development computer’s network. Option 3 If you want to test on devices whose UDID you don’t know (for example, you want to upload your app/game to a server and let whomever test it), you should use this option. However, you will need to go back to the iOS Provisioning Portal to get two more files and use those files in the Certificate and in the Provisioning Profile fields to publish an ad hoc .ipa file. One of the files has already been mentioned—an iOS distribution Certificate—and one I haven’t mentioned—an ad hoc distribution Provisioning Profile. I will list the steps needed to obtain both of those files in the upcoming “Distributing Your Game for iOS” section. Option 4 When your app/game’s development is complete and you’re ready to submit your app/game to the Apple App Store, you select this option. However, you’ll need to go back to the iOS Provisioning Portal to get two more files and use those files in the Certificate and in the Provisioning Profile fields to publish a deployment (Flash parlance) or distribution (Apple parlance) .ipa. I’ve already mentioned both of the needed files: a distribution Certificate and a distribution Provisioning Profile. I’ll list the steps needed to obtain both of those files in the upcoming “Distributing Your Game for iOS” section. For now, let’s finish discussing the Air for iOS Settings panel. 329
330 Chapter 8 n Developing and Distributing Games for iOS Devices Air for iOS Settings: Icons Tab Using this tab, you can add icons for your game to use when it is installed on your target device. You can create these icons using Flash (or your preferred graphics program). To use Flash, add a MovieClip (such as _tank mc), Button, or Graphic to the stage, size it so the largest dimension (width or height) matches the icon size, rightclick it, and click Export PNG Sequence. If you used a multiframe object, pick the PNG that you want to use for the icon and delete the rest. Add the icon sizes used by your target device (http://developer.apple. com/library/ios/#qa/qa1686/_index.html). (See Tables 8.1 and 8.2.) Table 8.1 iPad Icon Sizes Pre-Retina Retina Where Used 512 × 512 1024 × 1024 iTunes App Store 144 × 144 72 × 72 Home screen 48 × 48 96 × 96 Spotlight search 29 × 29 58 × 58 Settings Source: Apple, Inc. Table 8.2 iPhone Icon Sizes Pre-Retina Retina Where Used 512 × 512 1024 × 1024 iTunes App Store 114 × 114 57 × 57 Home screen 29 × 29 58 × 58 Spotlight search 29 × 29 58 × 58 Settings Source: Apple, Inc. Air for iOS Settings: Languages Tab If you’re using Flash Pro CS6 or better, you will have a Languages tab in your Air for iOS Settings panel. Otherwise, you won’t and you can ignore this section. The panel displayed by selecting this tab allows you to indicate which other languages your game supports. That information is added to your application descriptor and will be displayed by the Apple Store.
Distributing Your Game for iOS It’s important to notice that no language support is added to your game no matter what languages you check in this panel. Actually, adding language support is up to you, the developer. Distributing Your Game for iOS To distribute your game outside the Apple Store to users with unknown (to you) device UDIDs or to add your game to the Apple Store, you need more files than a development Certificate and a development Provisioning Profile. You will need a distribution Certificate and a distribution Provisioning Profile. Your distribution Provisioning Profile will indicate whether your game is for users with unknown UDIDs (called an ad hoc distribution Provisioning Profile) or whether your game is being submitted to the Apple Store (called a distribution Provisioning Profile). Your distribution Certificate is the same whether used with an ad hoc distribution Provisioning Profile or with a distribution Provisioning Profile. Here is the list of steps needed to create your distribution Certificate. 1. Go to https://developer.apple.com/devcenter/ios/index.action using a Mac or a virtual Mac. 2. Log in. 3. Click on the iOS Dev Center link. 4. Click on the iOS Provisioning Portal link (refer to Figure 8.4). 5. Click on the Certificates link in the left panel. 6. Click on the Distribution tab in the right panel. 7. Click on the Request Certificate button in the right panel. 8. Follow the directions using Keychain Access to create your distribution Certificate. (See Figure 8.5.) Apple has easy-to-follow directions to create a Certificate Signing Request that is used to create your Certificate. The steps needed to create a Certificate Signing Request appear in the right panel after Step 7. The only part I stumble on is when instructed to open Certificate Assistant. You will find it at the top left of your Mac’s screen after clicking Keychain Access, not in the Keychain Access window. 9. The right panel should change to show that your Certificate is pending issuance. Refresh the page, and you should see that your Certificate is issued and ready for download (refer to Figure 8.6). 10. Download your ios_distribution.cer by clicking the Download button. 331
332 Chapter 8 n Developing and Distributing Games for iOS Devices 11. Download an AppleWWDRCA.cer by clicking the *If You Do Not Have the WWDR Intermediate Certificate Installed, Click Here to Download Now link. 12. Open both files in Keychain Access (using the login Keychain). 13. Right-click your private key in Keychain Access (you may need to expand the iPhone Distribution Certificate shown in your Keychain Access window) and click Export. Make sure the File Format value is Personal Information Exchange (.p12) and click Save. You will be prompted for a password that you must remember. You will use that password each and every time you publish an iOS game for distribution, and you will use your .p12 (Distribution Certificate) file for all the apps/games you develop (until your certificate expires and you pay another $99 and follow these steps again). Here is the list of steps needed for a distribution Provisioning Profile and an ad hoc distribution Provisioning Profile. 1. Click the App IDs link in the left panel. 2. Click the New App ID button (toward the upper right). (Refer to Figure 8.9.) 3. Enter your App description (using only alphanumeric characters). 4. Select Team ID (unless you already have a Bundle Seed ID that you wish to reuse for related apps/games) in the Bundle Seed ID combobox. 5. Enter your Bundle Identifier. If you have a website, use com.yourwebsitename.yourappname for your Bundle Identifier. 6. Click Submit. 7. You should be back at the main App IDs link, and your new App ID (along with all your other App IDs) should be listed. (Refer to Figure 8.10.) 8. Click the Configure link if you want to add Apple Push Notification service and follow that multistep procedure, which has helpful instructions. 9. Click Provisioning in the left panel and, in the right panel, click the Distribution tab and click the New Profile button toward the upper right. (See Figure 8.17.) Notice the difference between Figures 8.11 and 8.17 and make sure you click the Distribution tab so you see something that looks like Figure 8.17.
Distributing Your Game for iOS Figure 8.17 iOS Provisioning Portal after clicking Provisioning in the left panel and then clicking the Distribution tab Source: Apple® Inc. 10. Check either App Store or Ad Hoc, depending on which distribution Provisioning Profile you need. 11. Enter a Provisioning Profile name, check whose Certificate will be used with the Provisioning Profile, and select the App ID from the combobox. Finally, click Submit. Note If you selected App Store in Step 10 in the list of steps needed for a distribution Provisioning Profile and an ad hoc distribution Provisioning Profile, all the (now optional) devices should be faded, indicating that you cannot check any device that will be used for testing because this a Provisioning Profile for Apple Store distribution, not development. However, the Select All link still works for no good reason that I can determine. 12. You should be back at the Provisioning link/Distribution tab, and the status (fourth column) of your new profile is probably pending. (Mine always is.) 333
334 Chapter 8 n Developing and Distributing Games for iOS Devices 13. Refresh the page. Your Provisioning Profile’s status should change to active, and a Download button should appear. If it doesn’t, go have a frappe and refresh the page when you return. 14. Click the Download button to download your Provisioning Profile. Save this to the directory where you will publish your game for the Apple Store or for ad hoc distribution. Now that you have all the files needed, you can publish your game. You will use exactly the same steps you used for publishing your game for development, except that instead of using your development Certificate and development Provisioning Profile, you must use your distribution Certificate and (ad hoc or not) distribution Provisioning Profile. If you used an ad hoc distribution Provisioning Profile, you don’t need to interact further with Apple. Just distribute your published .ipa to whomever you wish. According to Apple, you are limited to 100 iOS devices onto which your game can be installed. I’m not sure how they can ethically check that limit, but it’s Apple. They like to make and enforce rules. If you want to add your game to the Apple Store, then you have more steps to follow. Here are the initial steps. 1. Go to https://itunesconnect.apple.com and log in. 2. Click Manage Your Applications (see Figure 8.18).
Distributing Your Game for iOS Click Manage Your Applications to add your game to the Apple Store. Figure 8.18 After logging in to iTunes Connect, you should see this page. Source: Apple® Inc. 3. Click the Add New App button at the upper left. There are many more steps, but they are all very clearly explained. Click the question mark to the right of any field for which you need or want more information. You’ll need to upload some files (for example, at least one screenshot and at least one icon), and you’ll need to supply a URL where you offer support for your game. You’ll also need to indicate pricing information, and you’ll need to supply payment information (if you charge for your game). All of that and more is explained as you work your way through iTunes Connect. Also, you can download a clearly written Apple PDF that explains all the requirements and options for developers using iTunes Connect from http://developer.apple. com/library/ios/iTunesConnectGuide. 335
This page intentionally left blank
Chapter 9 Developing and Distributing Games for Android Devices In this chapter, I will examine the steps needed to develop, test, and deploy games for Android devices. I will start with a puzzle/logic game that I call Switcher. After going through the code for the Switcher game, I’ll cover technical details about how to test, publish, and distribute a game developed for an Android device. Those topics follow the Switcher game code and are the most important parts of this chapter. Switcher for Android Switcher is a simple game that requires the user to tap a series of switches to close a circuit that passes through each switch. Each switch has three positions: open, ready, and closed. If a switch is open, tapping it once changes it to ready. If a switch is ready, tapping it once changes it to closed. And if a switch is closed, tapping it once changes it to open. When all switches are closed, the circuit is closed and a light bulb illuminates. Except that tapping a switch not only changes the tapped switch’s position, but it also changes the position of immediately connected switches. (See Figure 9.1.) 337
338 Chapter 9 n Developing and Distributing Games for Android Devices An open switch A ready switch A closed switch The light bulb Figure 9.1 The introduction view of Switcher, showing three switches and one light bulb. Source: Adobe Systems Incorporated. This game, like most games of logic, doesn’t contain anything that should stress the CPU or GPU. The user sees only three screens: the intro view (shown in Figure 9.1), a game view where more switches are presented, and a game-over view where the results of the game view are presented.
Switcher for Android Notice that all three of those classes include a StageOrientationEvent.ORIENTATION_ CHANGE event listener and listener function to control the display when the Android device is rotated from vertical to horizontal (and vice versa). I believe that is the only additional thing that the tank combat game did not have. The document class (Main) controls which view is presented. Because both the intro view and the game view display working switches, they both use the Controller class, which controls all the switches and the light. Just as we did in the tank combat game, we’ll make use of pool classes to help ensure that there are no memory leaks. And, we’ll use a Data class for the four variables we share among the intro, game, and game-over views. Main package com.kglad { import flash.display.MovieClip; import flash.display.StageAlign; import flash.display.StageScaleMode import flash.events.Event; import flash.ui.Multitouch; import flash.ui.MultitouchInputMode; public class Main extends MovieClip { private var introView:IntroView; private var gameView:GameView; private var gameOverView:GameOverView; private var controller:Controller; public function Main() { //MT.init(this,3); // The usual inputMode unless you want to detect // gestures. Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT; // Initialize default data (min and max number // of switches). defaultDataF(); // These two lines of code are important when the // stage is rotated. Comment out each of them in // turn so you can see what happens when they are // not included, so if you run into the same // problem, you’ll recognize how to solve the problem. stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; // I used pools for everything displayed. Ensuring // that there are no memory leaks is much more // important for mobile games and applications than 339
340 Chapter 9 n Developing and Distributing Games for Android Devices // web-based games and applications because they // may not be re-started for months. TF_Pool.init(1); // Data.sliderMin and Data.sliderMax were already // initialized. Because solving these puzzles is // much easier for puzzles with an even number of // switches, I only allow an odd number of switches. Switch_Pool.init(Data.sliderMax*2+1); Light_Pool.init(1); Slider_Pool.init(1); Replay_Btn_Pool.init(1); // Instead of using a pool to retrieve and return // one Controller instance (which would have worked // here), I used a singleton class to limit this // game to one Controller instance. A singleton class // can also be used instead of a class with static // properties (like Data) to share data among other // classes. controller = Controller.getInstance(stage); IntroView_Pool.init(1); GameView_Pool.init(1); GameOverView_Pool.init(1); // Start the game by adding an IntroView instance to // the display. In this game there are no library // MovieClips corresponding to the Introview, // GameView or GameOverView. Everything displayed // is created or instantiated in those classes. addIntroViewF(); //MT.init(this,4); } private function addIntroViewF():void{ // The usual way to retrieve an instance from a pool. introView = IntroView_Pool.retrieveF(); // Add a listener to trigger the display of the // next view (a GameView instance). introView.addEventListener("startGameE",startGameF,false,0,true); addChild(introView); } private function startGameF(e:Event):void{ removeIntroViewF(); addGameViewF(); }
Switcher for Android private function removeIntroViewF():void{ // The usual way to return an instance to a pool. // Return the instance before removing from the // display. I had some trouble with the switches // because I wanted them all to return to their // closed state when they were returned to their // pool, and that didn’t work correctly unless // they were still in the display. IntroView_Pool.returnF(introView); removeChild(introView); introView.removeEventListener("startGameE",startGameF,false); } // The rest all follow the same format. Only the // names change. private function addGameViewF():void{ gameView = GameView_Pool.retrieveF(); gameView.addEventListener("gameOverE",gameOverF,false,0,true); addChild(gameView); } private function gameOverF(e:Event):void{ removeGameViewF(); addGameOverViewF(); } private function removeGameViewF():void{ GameView_Pool.returnF(gameView); removeChild(gameView); gameView.removeEventListener("gameOverE",gameOverF,false); } private function addGameOverViewF():void{ gameOverView = GameOverView_Pool.retrieveF(); gameOverView.addEventListener("replayE",replayF,false,0,true); addChild(gameOverView); } private function replayF(e:Event):void{ removeGameOverViewF(); addIntroViewF(); } private function removeGameOverViewF():void{ GameOverView_Pool.returnF(gameOverView); removeChild(gameOverView); gameOverView.removeEventListener("replayE",replayF,false); } private function defaultDataF():void{ // default Data values 341
342 Chapter 9 n Developing and Distributing Games for Android Devices Data.init(); } } } IntroView package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.text.TextField; import flash.events.StageOrientationEvent; import flash.events.TouchEvent; public class IntroView extends MovieClip { private var instructionsS:String; private var tf:TextField; private var switchNum:int = 3; private var switchA:Vector.<Switch>; private var light:Light; private var i:int; private var controller:Controller; private var slider:Slider; public function IntroView() { // This part of the instructions string never changes // so it can be added to the constructor, which executes // only once when this instance is added to its pool. // The last part of the string uses the number of // switches, so that has to be updated dynamically. instructionsS = "Tap a switch to change it from open to ready. Tap it again to change it from ready to closed and tap it again to open the switch. "; instructionsS += "Except, tapping a switch not only changes the tapped switch, it will change any switch immediately connected to the tapped switch.\n \n"; instructionsS += "The goal is to close all switches thereby closing the circuit and lighting the bulb. You can practice on this screen but three switches is no challenge.\n\n"; instructionsS += "Pick a more challenging number of switches to test your capacity for logic."; // The usual initializer and clean-up functions this.addEventListener(Event.ADDED_TO_STAGE,init,false,0, true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanUpF, false,0,true); }
Switcher for Android private function init(e:Event):void{ // A vector of the switches to allow the game player // to test how the switches work. switchA = new Vector.<Switch>(switchNum); // A listener to update the display if the device // is rotated stage.addEventListener(StageOrientationEvent.ORIENTATION_CHANGE, orientationChangeF,false,0,true); // Add the textfield used to display the instructions tfF(); // Add text to the instructions textfield tfTextF(); // Add the switches used in the IntroView instance switchesF(); // Add the light lightF(); // Retrieve a controller and send references to the // switches and light to the controller. controllerF(); // Add the slider used to control the number of switches // in the GameView instance sliderF(); } private function tfF():void{ tf = TF_Pool.retrieveF(); tf.y = 10; tf.x = 10; addChild(tf); } private function tfTextF():void{ // The last paragraph of the instructions is dynamic, // so I need to remove and re-add it when the user // changes the number of switches instructionsS = instructionsS.substr(0,instructionsS.lastIndexOf("\n\nCurrently ")); instructionsS += "\n\nCurrently "+Data.switchNum+" switches are selected. When you are ready, tap the light." tf.text = instructionsS; } private function switchesF():void{ // Here is where the switches are retrieved // and added. for(i=switchNum-1;i>=0;i––){ 343
344 Chapter 9 n Developing and Distributing Games for Android Devices switchA[i] = Switch_Pool.retrieveF(); addChild(switchA[i]); // The switches all start in the closed position. // That’s not important when there are only 3 // switches because no matter what initial state // the switches are in, there is always a solution: // move them all to the closed state. But when // there are more than 3, there are starting // states that have no solution. Avoiding // unsolvable states is done by starting all // the switches in the closed state (i.e., // starting with a solved puzzle) and then // clicking each switch either 0, 1, or 2 // times to obtain the puzzle start. There // is always a solution when creating a puzzle // this way. That solution is explained in // Controller. The switches layout is done in // controller. } } private function lightF():void{ light = Light_Pool.retrieveF(); // Use the light as a button to start the game. light.addEventListener(TouchEvent.TOUCH_TAP,startGameF,false,0,true); addChild(light); } private function controllerF():void{ // This is how to retrieve the only controller // instance when Controller is a singleton controller = Controller.getInstance(); // Use controller’s init() function to send switches // and light references and to indicate how from the // screen-top to start the display of the switches. controller.init(switchA,light,tf.height+tf.y); } private function sliderF():void{ // I’m using the same slider here that I made // for tank combat. slider = Slider_Pool.retrieveF(); slider.x = 10; slider.y = stage.fullScreenHeight-slider.height; addChild(slider); // Assign slider’s min and max properties and // touch listeners
Switcher for Android slider.min = Data.sliderMin; slider.max = Data.sliderMax; slider.addEventListener(TouchEvent.TOUCH_BEGIN,explanationF, false,0,true); slider.addEventListener(TouchEvent.TOUCH_END,explanationF, false,0,true); slider.addEventListener(TouchEvent.TOUCH_MOVE,sliderChangeF, false,0,true); // Because I only want to allow an odd number of // switches, I convert the slider.value to the // number of switches by multiplying by 2 and // adding one. // i.e., Data.switchNum = 2*slider.value+1; // Setting the value property of slider given // Data.switchNum requires the inverse, subtract // 1 and divide the result by 2. slider.value = (Data.switchNum-1)/2; } private function explanationF(e:TouchEvent):void{ // TouchEvent.TOUCH_BEGIN and TouchEvent.TOUCH_END // events use this listener function if(e.type==TouchEvent.TOUCH_BEGIN){ tf.text = "Adjust the number of switches from "+(Data. sliderMin*2+1)+" to "+(Data.sliderMax*2+1)+". Then tap the light to start.\n \nCurrently the number of switches is "+(2*int(e.currentTarget.value)+1); } else { // On TouchEvent.TOUCH_END, display the text // in tfTextF() tfTextF(); } } private function sliderChangeF(e:Event):void{ // Update Data.switchNum when the slider changes. tf.text = "The current number of switches is "+(2*int(e.currentTarget.value)+1)+" out of a maximum of "+(2*Data.sliderMax+1); Data.switchNum = 2*int(e.currentTarget.value)+1; } private function startGameF(e:TouchEvent):void{ // Dispatched to introView in Main dispatchEvent(new Event("startGameE")); } private function orientationChangeF(e):void{ // Adjust tf’s width when there’s a device rotation. tf.width = stage.fullScreenWidth-20; 345
346 Chapter 9 n Developing and Distributing Games for Android Devices // Adjust slider’s y slider.y = stage.fullScreenHeight-slider.height; // Have controller make changes to the switches and // light and wires display. In addition, there’s a // TF instance added by controller when a GameView // instance is displayed. And it needs to be // adjusted when there’s a device rotation. // It would have been more natural to add the // TF instance in GameView, but because tapping // a switch requires an update to the TF instance // text, I decided it was easier to add the TF // instance in controller rather than dispatch // events from controller to GameView. controller.orientationChangeF(tf.y+tf.height); } private function cleanUpF(e:Event):void{ // Remove the listeners that are no longer needed // and return all the display objects to their pools. light.removeEventListener(TouchEvent.TOUCH_TAP,startGameF,false); stage.removeEventListener(StageOrientationEvent.ORIENTATION_CHANGE, orientationChangeF,false); // return before removing or else goto is funky with // the currentFrame reporting what’s expected but not // the same as displayed. for(i=switchNum-1;i>=0;i––){ Switch_Pool.returnF(switchA[i]); removeChild(switchA[i]); } removeChild(slider); slider.removeEventListener(TouchEvent.TOUCH_BEGIN,explanationF,false); slider.removeEventListener(TouchEvent.TOUCH_END,explanationF,false); slider.removeEventListener(TouchEvent.TOUCH_MOVE,sliderChangeF,false); Slider_Pool.returnF(slider); Light_Pool.returnF(light); removeChild(light); TF_Pool.returnF(TF(tf)); removeChild(tf); controller.cleanUpF(); } } }
Switcher for Android GameView package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.StageOrientationEvent; import flash.utils.getTimer; public class GameView extends MovieClip { private var switchNum:int; private var switchA:Vector.<Switch>; private var light:Light; private var controller:Controller; private var i:int; private var tf:TF; public function GameView() { this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanUpF,false,0, true); } private function init(e:Event):void{ switchNum = Data.switchNum; switchA = new Vector.<Switch>(switchNum); stage.addEventListener(StageOrientationEvent.ORIENTATION_ CHANGE, orientationChangeF,false,0,true); // Retrieve the needed switches. switchesF(); // Retrieve the Light instance. lightF(); // Access the singleton Controller instance. controllerF(); // Retrieve a textfield used to give user feedback tfF(); } private function switchesF():void{ for(i=switchNum-1;i>=0;i––){ switchA[i] = Switch_Pool.retrieveF(); addChild(switchA[i]); // Layout done in controller } } private function lightF():void{ light = Light_Pool.retrieveF(); // Listen for tfUpdateE dispatched from controller 347
348 Chapter 9 n Developing and Distributing Games for Android Devices // when a switch is tapped. light.addEventListener("tfUpdateE",tfUpdateF,false,0,true); addChild(light); // Layout done in controller } private function controllerF():void{ // Access the Controller instance and listen // for a gameOverE event. controller = Controller.getInstance(); controller.addEventListener("gameOverE",gameOverF,false,0,true); // Call controller’s public init() function passing // a 4th parameter indicating controller is // initialized from the GameView instance and needs // tf updates. controller.init(switchA,light,0,"gameView"); } private function tfF():void{ tf = TF_Pool.retrieveF(); addChild(tf); tfUpdateF(); tf.x = 10; // light.x is not ready for one frame tick. tf.addEventListener(Event.ENTER_FRAME,oneTickF,false,0,true); } function oneTickF(e:Event):void{ tf.width = light.x-30; tf.y = stage.fullScreenHeight-tf.height; tf.removeEventListener(Event.ENTER_FRAME,oneTickF,false); } private function tfUpdateF(e:Event=null):void{ // Data.tapNum is updated in controller, tfUpdateE // is dispatched in controller, and tfUpdateF() is // called here displaying the updated Data.tapNum // to the user. tf.text = "This "+switchNum+"-switch puzzle can be solved with "+Data.tapMin+" or less taps. So far, you have made "+Data.tapNum+" taps.\n\nTap the light to restart this puzzle. Double tap the light to quit this puzzle."; } private function gameOverF(e:Event):void{ // No need to create a new event. Just dispatch the // same gameOverE to listener in Main. dispatchEvent(e); } private function orientationChangeF(e):void{
Switcher for Android // Change layout of switches, light, and wires // in controller controller.orientationChangeF(0); // Change tf layout here tf.width = light.x-30; tf.y = stage.fullScreenHeight-tf.height; } private function cleanUpF(e:Event):void{ // The usual cleanup, returning objects to their // pools and removing added event listeners controller.removeEventListener("gameOverE",gameOverF,false); stage.removeEventListener(StageOrientationEvent.ORIENTATION_CHANGE, orientationChangeF); for(i=switchNum-1;i>=0;i––){ Switch_Pool.returnF(switchA[i]); removeChild(switchA[i]); } Light_Pool.returnF(light); removeChild(light); TF_Pool.returnF(tf); removeChild(tf); controller.cleanUpF(); } } } Controller package com.kglad{ import flash.display.Stage; import flash.display.Sprite; import flash.display.MovieClip; import flash.events.TouchEvent; import flash.utils.Timer; import flash.utils.getTimer; import flash.events.TimerEvent; import flash.events.Event; import flash.events.EventDispatcher; public class Controller extends EventDispatcher{ private static var controllerInstance:Controller; private static var allowInstantiation:Boolean; private var i:int; private var j:int; private var initX:int = 10; private var initY:int; 349
350 Chapter 9 n Developing and Distributing Games for Android Devices private var nextX:int; private var nextY:int; private var switchW:int; private var switchH:int; private var switchGapX:int = 20; private var switchGapY:int; private var switchA:Vector.<Switch>; private var switchStateA:Vector.<int>; private var switchA_len:int; private var light:Light; private var stageW:int; private var stageH:int; private var graphicSP:Sprite; private var tapIndex:int; private var viewS:String; private var closedCircuitBool:Boolean; private var gameOverTimer:Timer; private var rNum:int; private var tapMin:int; private var prevLightTap:int; private var tfReady:Boolean; private static var _stage:Stage; // This is a singleton class. It has no display representation // and is not added to the display list. But I needed a // reference so I could use the drawing methods of the Graphics // class to create the wires, so I needed to create and add // a display object, the Sprite graphicSP, to the display // list, too. public static function getInstance(stageVar:Stage=null):Controller { if (controllerInstance == null) { allowInstantiation=true; controllerInstance = new Controller(); allowInstantiation=false; } if(stageVar){ // My stage reference passed from Main. _stage = stageVar; } return controllerInstance; } public function Controller():void { if (! allowInstantiation) {
Switcher for Android throw new Error("Error: Instantiation failed: Use SingletonDemo.getInstance() instead of new."); } } public function init(_switchA:Vector.<Switch>,_light:Light,_nextY:int,_viewS:String=""):void{ // Used to determine if all the switches are closed. closedCircuitBool = false; // Used to prevent communication with tf in // GameView instance until tf is ready. tfReady = false; // init() is called from IntroView and GameView // instances. When called from the GameView instance, // the _viewS parameter is passed and used to indicate // the controller is for the GameView instance. viewS = _viewS; // Get a Controller instance reference to _switchA switchA = _switchA; // Variables used to position the switches and // draw the wires. switchW = switchA[0].width; switchH = switchA[0].height+10; // For optimizing switchA_len = switchA.length; // Used if the initial state of the switches is // needed (if and when resetting). switchStateA = new Vector.<int>(switchA_len); // Get a Controller instance reference to _light light = _light; // Used to position the switches switchGapY = switchH+30; // The sprite used for the wires/drawing if(!graphicSP){ graphicSP = new Sprite(); } _stage.addChild(graphicSP); // Instead of attaching listeners to each switch, I // check for switch tap in stageTapF(). _stage.addEventListener(TouchEvent.TOUCH_TAP,stageTapF,false,0,true); // Used when GameView instance uses controller. // Only one gameOverTimer created and never cleared // from memory while this game exists. if(!gameOverTimer){ gameOverTimer = new Timer(3000,1); 351
352 Chapter 9 n Developing and Distributing Games for Android Devices } else { gameOverTimer.reset(); } gameOverTimer.addEventListener(TimerEvent.TIMER,gameOverF,false,0,true); // y property of the first row of switches. initY = _nextY+4*switchGapY/5; // Randomize the initial state of the switches randomizeSwitchesF(); // Initialize the number of switch taps Data.tapNum = 0; // Lay out the switches, the light, and the wires layoutF(); if(viewS=="gameView"){ // GameView instance’s tf is ready. tfReady used // in updateGameViewTF() so no updates to tf are // dispatched while randomizing switches and // before light.x is updated. tfReady = true; light.addEventListener(TouchEvent.TOUCH_TAP,lightTapF,false,0,true); } } private function updateGameViewTF():void{ if(tfReady){ light.dispatchEvent(new Event("tfUpdateE")); } } private function lightTapF(e:TouchEvent):void{ // If double tap, exit game. if(getTimer()-prevLightTap<1000){ // Data.tapNum is used in GameOverView instance // to display user feedback Data.tapNum = -1; // Dispatched to controller in GameView instance, // which then dispatches the event to Main. dispatchEvent(new Event("gameOverE")); } else { // Otherwise, reset the switches, update Data.tapNum, // and update tf in GameView. resetF(); Data.tapNum = 0; updateGameViewTF(); } // Used to detect double tap above. prevLightTap = getTimer();
Switcher for Android } private function resetF():void{ // Reset the switches by first "closing" them all... closeAllSwitchesF(); // ...and then duplicating the initial randomizing taps. // (See randomizeSwitchesF below.) i needs to be local // to resetF() because it is changed in checkCircuitF(), // which is called from stageTapF(), the // TouchEvent.TOUCH_TAP listener function. for(var i:int=switchA_len-1;i>=0;i––){ for(var j:int=switchStateA[i]-1;j>=0;j––){ switchA[i].dispatchEvent(new TouchEvent(TouchEvent.TOUCH_TAP)); } } } private function closeAllSwitchesF():void{ for(i=switchA_len-1;i>=0;i––){ if(switchA[i].currentFrameLabel!="closed"){ switchA[i].gotoAndStop("closed"); } } } private function randomizeSwitchesF():void{ // Initialize a counter to track how many "taps" it // would take to solve the puzzle by "brute force," // i.e., by reversing the taps on each switch. // You can always reverse the taps: If a switch is // not tapped, it needs no reversing. If a switch is // tapped once, tapping twice more will undo its // initial tap. If a switch is tapped twice, tapping // once more will undo the initial two taps. tapMin // tracks the number of reversing taps and provides // an upper limit to the number of taps needed to // solve the puzzle. tapMin = 0; // Generate the random taps for each switch. I need to // make i local to this function because it is changed // in checkCircuitF(), which is called from stageTapF(). for(var i:int=switchA_len-1;i>=0;i––){ // rNum is random int 0,1, or 2, used to // determine how many taps switchA[i] // should emulate rNum = int(3*Math.random()); 353
354 Chapter 9 n Developing and Distributing Games for Android Devices // switchStatA is used if resetting to the // original puzzle is desired. switchStateA[i] = rNum; // tapMin incremented. If rNum=0, no reversing // taps are needed explaining the modulo 3. tapMin += (3-rNum)%3; // Dispatch the events changing the switch // states. for(j=rNum-1;j>=0;j––){ switchA[i].dispatchEvent(new TouchEvent(TouchEvent.TOUCH_TAP)); } } // Ensure at least one of the switches is // not closed. closedCircuitBool = true; for(i=switchA_len-1;i>=0;i––){ if(switchA[i].currentFrameLabel!="closed"){ closedCircuitBool = false; break; } } // If closedCircuitBool is still true, all the switches // are closed (and the light is on) -> re-randomize // the switches. if(closedCircuitBool){ // Re-randomize the switches. randomizeSwitchesF(); // And turn off the light! light.gotoAndStop("off"); } // Data.tapMin is used in the GameView and // GameOverView instances for user feedback. Data.tapMin = tapMin; } private function layoutF():void{ layoutSwitchesF(); layoutLightF(); layoutWiresF(); } private function layoutSwitchesF():void{ // Use the display dimensions to lay out // the display. stageW = _stage.fullScreenWidth;
Switcher for Android stageH = _stage.fullScreenHeight; // The x,y for the top-leftmost switch nextX = initX; nextY = initY; // This is one of two places where I found it easier // to iterate through switchA from beginning to end // rather than from end to beginning. for(i=0;i<switchA_len;i++){ switchA[i].x = nextX; switchA[i].y = nextY; // nextX = nextX+switchA[i] + switchGapX and // must be < stageW - switchA[i+1].width so // the next switch fits on-stage // That only matters if there is a next switch, // so only check that if i is not the last switch if(i<switchA_len-1){ // Check that the next switch will fit // on-stage if placed to the right of // the previous switch. if(nextX+switchGapX+2*switchW<stageW){ nextX += switchGapX+switchW; } else { // If it won’t fit, place the // next switch on the next y, // initial x. nextX = initX; nextY += switchGapY; } } } } private function layoutLightF():void{ // Position the light at the lower right of // the display. light.x = stageW - light.width-10; light.y = stageH - 10; } private function layoutWiresF():void{ // Finally, draw the wires to connect the // switches and light. with(graphicSP.graphics){ // Clear anything previously drawn. clear(); 355
356 Chapter 9 n Developing and Distributing Games for Android Devices // Define a lineStyle lineStyle(0,0x000000); // Start the line at the left of the display // (x=0), y equal the first switches y. Notice // the switch and light have registration // points where I want wires to "enter." moveTo(0,switchA[0].y); // Draw to the first switch. lineTo(switchA[0].x,switchA[0].y); // This is the second place I found it // easier to iterate from beginning to end. for(i=0;i<switchA_len;i++){ // Move to the right of the switch // where the wire will "exit." moveTo(switchA[i].x+switchW,switchA[i].y); // If there is a next switch, connect to it. // Otherwise, connect to the light. if(i<switchA_len-1){ // If the next switch has the same y, // connect directly to that switch. if(switchA[i+1].y==switchA[i].y){ lineTo(switchA[i+1].x,switchA[i +1].y); } else { // Otherwise, I have a // right-to-left connection // between the two switches. RtoL(switchA[i],switchA[i+1]); } } else { // Connect to light. If the light is // to the right of the wire’s exit: if(switchA[i].x+switchW<light.x){ // Draw a horizontal wire just // to the left of the light lineTo(light.x-initX/2,switchA[i].y); // Draw a vertical wire to // the light’s y (line down // to light’s y). lineTo(light.x-initX/2,light.y); // Connect to the light (line // right to the light). lineTo(light.x,light.y);
Switcher for Android } else { // The light is to the left // of the switch. Use a // right-to-left connection. RtoL(switchA[i],light); } } } } } private function RtoL(s1:Switch,obj2:MovieClip):void{ with(graphicSP.graphics){ // line right just distal to s1 switch lineTo(s1.x+switchW+initX/2,s1.y); // line down between s1 and obj2 lineTo(s1.x+switchW+initX/2,s1.y+switchH/4); // line left just proximal to obj2 lineTo(obj2.x-initX/2,s1.y+switchH/4); // line down to obj2 lineTo(obj2.x-initX/2,obj2.y); // line right to obj2 lineTo(obj2.x,obj2.y); } } private function stageTapF(e:TouchEvent):void{ // The stage was tapped. Check if the TouchEvent // target is a switch and, if so, which one. tapIndex = switchA.indexOf(e.target); // if tapIndex>-1, a switch was tapped. if(tapIndex>-1){ // The Switch class has a public nextF() // that changes the switch state. e.target.nextF(); // Increment Data.tapNum Data.tapNum++; // If this controller is being used by the // GameView instance, update tf to show the // user another switch tap has been detected // and display Data.tapNum (among other // things). if(viewS=="gameView"){ updateGameViewTF(); } // If this is not the last switch, change the 357
358 Chapter 9 n Developing and Distributing Games for Android Devices // state of the next (in switchA) switch. if(tapIndex+1<switchA_len){ switchA[tapIndex+1].nextF(); } // If this is not the first switch, change the // state of the previous (in switchA) switch. if(tapIndex-1>=0){ switchA[tapIndex-1].nextF(); } // Check if circuit closed and puzzle solved. checkCircuitF(); } } private function checkCircuitF():void{ // Initialize closedCircuitBool closedCircuitBool = true; // Loop through the switches looking for one that // is not "closed." for(i=switchA_len-1;i>=0;i––){ if(switchA[i].currentFrameLabel!="closed"){ closedCircuitBool = false; break; } } if(closedCircuitBool){ // Circuit is closed. Puzzle is solved. closedCircuitF(); } } private function closedCircuitF():void{ // Display light turned on. light.gotoAndPlay("on"); if(viewS=="gameView"){ // Exit the game after displaying the light // "on" for a few seconds. gameOverTimer.start(); } } private function gameOverF(e:TimerEvent):void{ // Dispatch "gameOverE" event to GameView instance, // which then dispatches the event to Main. dispatchEvent(new Event("gameOverE")); } public function orientationChangeF(_nextY:int):void{
Switcher for Android // Adjust the layout using the current screen width // and height (defined in layoutSwitchesF). layoutF(); } public function cleanUpF():void{ // Nothing needs to be returned to a pool here. // The only object created is graphics, and that will // be reused each time the only Controller instance’s // init() function is called. It only needs to be // removed from the display. _stage.removeChild(graphicSP); // Remove all the listeners created here. gameOverTimer.removeEventListener(TimerEvent.TIMER,gameOverF, false); if(viewS=="gameView"){ light.removeEventListener(TouchEvent.TOUCH_TAP,lightTapF,false); } _stage.removeEventListener(TouchEvent.TOUCH_TAP,stageTapF,false); } } } GameOverView // Nothing new here package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.StageOrientationEvent; import flash.display.Sprite; public class GameOverView extends MovieClip { private var tf:TF; private var replay_btn:Replay_Btn; public function GameOverView() { this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanUpF,false,0, true); } private function init(e:Event):void{ stage.addEventListener(StageOrientationEvent.ORIENTATION_ CHANGE, orientationChangeF,false,0,true); tfF(); replayButtonF(); 359
360 Chapter 9 n Developing and Distributing Games for Android Devices } private function replayButtonF():void{ replay_btn = Replay_Btn_Pool.retrieveF(); addChild(replay_btn); replay_btn.x = (stage.fullScreenWidth-replay_btn.width)/2; replay_btn.y = tf.y+tf.height+100; replay_btn.addEventListener("replayE",replayF,false,0,true); } private function replayF(e:Event):void{ dispatchEvent(e); } private function tfF():void{ tf = TF_Pool.retrieveF(); addChild(tf); if(Data.tapNum<0){ tf.text = "You failed to solve the previous "+Data.switchNum+"-switch puzzle.\n\nTap the button below to replay."; } else { tf.text = "You solved the previous "+Data.switchNumpdf +"-switch puzzle using "+Data.tapNum+" taps. This puzzle could be solved using "+Data.tapMin+" taps or less.\n\nYou scored "+Math.min(100,int(100*Data.tapMin/Data.tapNum))+"%.\n\nTap the button below to replay."; } tf.width = stage.fullScreenWidth-20; tf.y = 100; } private function orientationChangeF(e):void{ tf.y = 100; tf.x = 10; tf.width = stage.fullScreenWidth-20; replay_btn.x = (stage.fullScreenWidth-replay_btn.width)>>1; replay_btn.y = tf.y+tf.height+100; } private function cleanUpF(e:Event):void{ stage.removeEventListener(StageOrientationEvent.ORIENTATION_CHANGE, orientationChangeF,false); TF_Pool.returnF(tf); removeChild(tf); replay_btn.removeEventListener("replayE",replayF,false); Replay_Btn_Pool.returnF(replay_btn); removeChild(replay_btn); } } }
Switcher for Android Data package com.kglad { public class Data { // variables with default values below private static var _switchNum:int; private static var _sliderMin:int private static var _sliderMax:int; private static var _tapNum:int; private static var _tapMin:int; public function Data() { // constructor code } public static function init():void{ // slider varies from initial _switchNum to // _switchMax. The number of switches is // 2*slider.value+1; _sliderMin = 2; _sliderMax = 19; _switchNum = 5; _tapNum = 0; } public static function set switchNum(n:Number):void{ _switchNum = n; } public static function get switchNum():Number{ return _switchNum; } public static function get sliderMin():Number{ return _sliderMin; } public static function get sliderMax():Number{ return _sliderMax; } public static function set tapNum(n:Number):void{ _tapNum = n; } public static function get tapNum():Number{ return _tapNum; } 361
362 Chapter 9 n Developing and Distributing Games for Android Devices public static function set tapMin(n:Number):void{ _tapMin = n; } public static function get tapMin():Number{ return _tapMin; } } } Switch package com.kglad { import flash.display.MovieClip; public class Switch extends MovieClip { public function Switch() { } // This public function is used to update the // switch state. public function nextF():void{ if(this.currentFrame<3){ this.nextFrame(); } else { this.gotoAndStop("open"); } } } } TF package com.kglad { import flash.text.TextField; import flash.text.TextFormat; import flash.events.Event; public class TF extends TextField { private var tfor:TextFormat; public function TF() { tfor = new TextFormat(); var verdana:VerdanaReg = new VerdanaReg(); tfor.font = verdana.fontName; tfor.size = 12; tfor.leading = -1; this.embedFonts = true; // Again, use defaultTextFormat when assigning a
Switcher for Android // TextFormat instance before text is assigned. // After text is assigned, use the // setTextFormat() method. this.defaultTextFormat = tfor; this.multiline = true; this.wordWrap = true; this.autoSize = "left"; this.border = true; this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ this.width = stage.fullScreenWidth-20; } } } Replay_Btn package com.kglad { import flash.display.Sprite; import flash.events.TouchEvent; import flash.events.Event; public class Replay_Btn extends Sprite{ public function Replay_Btn() { this.addEventListener(TouchEvent.TOUCH_TAP,tapF,false,0,true); with(this.graphics){ beginFill(0xE08000); drawRect(0,0,50,50); endFill(); } } private function tapF(e:TouchEvent):void{ dispatchEvent(new Event("replayE")); } } } Slider package com.kglad { // There is no change in Slider import flash.display.MovieClip; import flash.ui.Multitouch; import flash.ui.MultitouchInputMode; import flash.events.TouchEvent; import flash.events.Event; 363
364 Chapter 9 n Developing and Distributing Games for Android Devices import flash.geom.Rectangle; public class Slider extends MovieClip { private var leftX; private var rightX; private var _value:Number; private var _max:Number; private var _min:Number; public function Slider() { // This is a horizontal slider, so define the left // and right extremes of the thumb (which has a // center x registration point). // When the slider thumb.x = leftX, _value should // be _min. When the slider thumb.x = rightX, _value // should be _max. Linear interpolation is used to // determine _value when thumb.x is between // leftX and rightX. leftX = this.track_mc.getRect(this).x; rightX = this.track_mc.getRect(this).width; Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT; this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ // Two other touch events defined exactly like you // would expect this.thumb.addEventListener(TouchEvent.TOUCH_BEGIN, touchBeginF); this.thumb.addEventListener(TouchEvent.TOUCH_MOVE, touchMoveF); this.thumb.addEventListener(TouchEvent.TOUCH_END, touchEndF); } private function touchBeginF(e:TouchEvent):void{ // TouchEvents have a touchPointID, which is // necessary to keep track of different // simultaneous touch points when // Multitouch.inputMode = MultitouchInputMode.GESTURE. // With single touch points like in this game, // keeping track of touch points is not critical, // but it is required for the startTouchDrag() // and stopTouchDrag() methods. The other // parameters in startTouchDrag() are the same // as startDrag(). e.currentTarget.startTouchDrag(e.touchPointID, false, new Rectangle (0,0,this.rightX,0)); }
Switcher for Android private function touchMoveF(e:TouchEvent):void{ // Update _value valueF(); } private function touchEndF(e:TouchEvent):void{ valueF(); e.currentTarget.stopTouchDrag(e.touchPointID); } private function valueF():void{ // Linear interpolation to find _value given thumb.x _value = (_min*rightX-_max*leftX+(_max_min)*this.thumb.x)/(rightX-leftX); } public function get value():Number{ return _value; } public function set value(n:Number):void{ _value = n; // Another use of linear interpolation to find // thumb.x given _value this.thumb.x = (_value*(rightX-leftX)-_min*rightX_max*leftX)/(_max-_min); } public function set max(n:Number):void{ _max = n; } public function get max():Number{ return _max; } public function set min(n:Number):void{ _min = n; } public function get min():Number{ return _min; } } } // All but three of the pool classes are exactly the same except // for data types. The three pool classes with a little difference // are the following, which encode some housekeeping in returnF() 365
366 Chapter 9 n Developing and Distributing Games for Android Devices Switch_Pool package com.kglad{ public class Switch_Pool { private static var pool:Vector.<Switch>; public static function init(poolSize:int):void { pool = new Vector.<Switch>(poolSize); for(var i:int=poolSize-1;i>=0;i––){ pool[i] = new Switch(); } } public static function retrieveF():Switch { if (pool.length>0) { return pool.pop(); } else { // this branch should not execute. return new Switch(); } } public static function returnF(sw:Switch):void { if(sw.currentFrameLabel!="closed"){ sw.gotoAndStop("closed"); } pool.push(sw); } } } Light_Pool package com.kglad{ public class Light_Pool { private static var pool:Vector.<Light>; public static function init(poolSize:int):void { pool = new Vector.<Light>(poolSize); for(var i:int=poolSize-1;i>=0;i––){ pool[i] = new Light(); } } public static function retrieveF():Light { if (pool.length>0) { return pool.pop(); } else { // this branch should not execute. return new Light();
Testing an Android Game } } public static function returnF(light:Light):void { if(light.currentFrameLabel!="off"){ light.gotoAndStop("off"); } pool.push(light); } } } TF_Pool package com.kglad{ public class TF_Pool { private static var pool:Vector.<TF>; public static function init(poolSize:int):void { pool = new Vector.<TF>(poolSize); for(var i:int=poolSize-1;i>=0;i––){ pool[i] = new TF(); } } public static function retrieveF():TF { if (pool.length>0) { return pool.pop(); } else { // this branch should not execute. trace("should not execute in TF_Pool"); return new TF(); } } public static function returnF(tf:TF):void { tf.text = ""; pool.push(tf); } } } Testing an Android Game Hopefully you have an Android device you can use for testing. If so, or if you have Flash CS6 or better, you can skip the sections on Android Emulators, Android Debug Bridge, and Adobe Air Developer Tool. 367
368 Chapter 9 n Developing and Distributing Games for Android Devices AIR Debug Launcher I recommend using the AIR Debug Launcher (if you have Flash Pro CS6 or better) to test an Android game, coupled with intermittent testing on an Android device. With Flash Pro CS6 or better, the development and testing processes for Android and iOS are similar. Click Control > Test Movie > in AIR Debug Launcher (Mobile) and test your game. After you resolve coding bugs, test it on an Android device to check performance. You’ll learn how to publish an .apk file and load that onto an Android device in the upcoming “AIR for Android Settings: Deployment Tab” section. For more information about the AIR Debug Launcher, read the “Testing an iOS Game” section toward the end of Chapter 8, “Developing and Distributing Games for iOS Devices.” As a reminder, you will need to check the Auto Orientation option in the Accelerometer menu’s Settings subpanel if you want the AIR display to reflect vertical and horizontal device orientation as you change the Z-rotation in the Simulator Controller. You open that subpanel by clicking the Settings icon to the right of the Reset icon (refer to Figure 8.3). If you have Flash CS5 or Flash CS5.5 and you cannot use the AIR Debug Launcher, you must use an Android device or an Android emulator for testing. Using an Android device is preferable because setting up Android emulators is timeconsuming, and their performance is terrible. However, if you have no other option for testing your Android game, here is how to set up and use Android emulators. I recommend you read the next three sections—“Android Emulators,” “Android Debug Bridge (ADB),” and “Adobe AIR Developer Tool (ADT)”— only if you have no other option for testing and debugging your Android game. Android Emulators Whereas there is no emulator for iOS (at this time), there is one for the Android OS. At the time of this writing, I found it inferior to the mobile simulator that Flash Pro CS6 offers. But you might find it better than nothing if you don’t have an Android device to connect to your computer, and you have Flash Pro CS5 or CS5.5. Broadly speaking, to set up emulators: 1. Download and install the Android SDK. 2. Create one or more Android OS emulators. 3. Load Air into your emulators. 4. Load your Flash game into your emulators.
Testing an Android Game If that sounds like a hassle, you have a misimpression. It is much more than a hassle because you have to use the command-line interpreter to load Air and your game into your emulators. If you’ve never used a command-line interpreter, you have an opportunity to see what home computing was like in the dark ages before Apple created a computer with a graphics interface that Microsoft quickly copied, bringing home computing to the masses. And, if you have used a command-line interpreter in the past, this should give you a reminder of why you almost certainly avoided it once you discovered the ease of a graphics interface. I will sketch the steps needed to install and prepare emulators for Android, but if you can get your hands on an Android device or you have Flash Pro CS6, don’t bother. If you have CS6, use its simulator. If you have an Android device, use it. And ideally, you’ll have both, so you can use both for testing. To start, download the Android SDK: http://developer.android.com/sdk/index.html (refer to Figure 9.2). This is only part of the download. Windows users should download the .exe so it can check for the needed Java SE Development Kit. Download for windows Download for mac Figure 9.2 Source: Google® Corporation. 369
370 Chapter 9 n Developing and Distributing Games for Android Devices That first download will extract and install the basic tools used to download the rest of the SDK. So, after installing it, navigate to your install directory and run SDK Manager.exe. You will be offered an assortment of packages to download and install. Check the Tools package, the Android OS versions you want to emulate, and the extras you want to access. There is no reason to select any OS versions less than 2.2 if you’re only developing Android apps using Flash, because version 2.2 was the first to support Adobe Air. You can delete installed packages, and you can download and install more packages at any time by re-running SDK Manager.exe. When you’re ready, click the Install Package button. It may take some time to complete the downloads and installation, so take a break or work on something else while you’re waiting. When that is complete, click AVD Manager.exe (Android Virtual Device Manager) if you want to create an emulator (or emulators). You’ll probably want to create a desktop shortcut to the AVD Manager because you’ll use this every time you want to start an emulator. You might as well do that now. Click a target OS to emulate from the combobox and then create an emulator name that makes it easy to remember which OS you’re emulating. There are a number of name restrictions, so if the Create AVD button isn’t clickable, check just above the button for error specifics. Select an SD card size (1024 MB works) and click Create AVD unless you want to change one of the other default settings. Your emulators should be listed in the AVD Manager, where you can select one and click Start. A Launch Options panel should open, allowing you to launch your emulator. Your emulator should finally start and be ready for test clicks after a short delay. You can click your Home button to see how awful the emulator’s performance is, at least on Windows. If you aren’t discouraged yet and you still want to proceed, you must install Adobe AIR on each of your emulators. To do that, you must download the Adobe AIR runtime application for Android, which is part of the Adobe AIR SDK (www.adobe.com/ devnet/air/air-sdk-download.html). Download and extract those files. The file you want to install on your emulators (Runtime.apk) is in the AdobeAIRSDK/runtimes/air/android/emulator directory. You are now ready to install the Air runtime on your emulators and then install your games. You have two ways to install Air and games onto your emulators. You can use the Android Debug Bridge (ADB) or the Adobe AIR Developer Tool (ADT).
Testing an Android Game Note Android has an Eclipse plug-in called Android Development Tools (usually denoted by ADT), which I won’t mention again. Just be aware when you’re reading elsewhere that ADT may not refer to the AIR Developer Tool. Both ADB and ADT are command-line tools, and they are equally aggravating to use. All command-line tools, including those two, must be executed from the commandline interpreter. If you’re using Windows, you can open the command-line interpreter by entering cmd in your OS run panel (press the Windows key + R). If that window is small (and especially if it’s less than 800px wide), click the command panel’s icon at panel’s upper left, click Properties, and adjust the settings so you see a decent-sized interpreter panel in the preview. I couldn’t make sense of the width/height numbers, but the preview was helpful. If you’re using a Mac, open Applications/Utilities/Terminal. Resizing the panel is obvious on a Mac. Type help at the command-line interpreter prompt. You will see a list of available commands. For help using a specific command, type help followed by the command. The command-line interpreter is not case-sensitive. Android Debug Bridge (ADB) Now, you can either navigate (in the command-line interpreter) to ADB or add it to your system path. If you’re going to use ADB more than a few times, you should add it to your system path. To understand why you should add it to your system path, check out the command required to execute ADB and have it add the AIR runtime to my emulator-5556: C:\Program Files (x86)\Android\android-sdk\platform-tools\ADB -s emulator-5556 install F:\Downloads\AdobeAir\AdobeAIRSDK\runtimes\air\android\emulator\Runtime.apk Your command will be different. However, it won’t be much shorter, if at all. You can also use local paths to one or both files, so by navigating to one of the two (ADB.exe or Runtime.apk) directories, you can significantly abbreviate that command. But, it is still tediously long and easy to make typos that are time-consuming to fix. To add the ADB path to your system path in Windows, click Start and then rightclick My Computer. Click Properties > Advanced System Settings > Environment 371
372 Chapter 9 n Developing and Distributing Games for Android Devices Variables. In the System Variables field, click Path > Edit and then click the arrow to the last entry of the Variable value line. Append a semicolon and type the full path to your platform-tools directory. Finally, click OK. You can now type ADB from anywhere in your command-line editor, and ADB in the platform-tools directory will execute. So, to install the Adobe AIR runtime on one of your emulators, start an emulator (using AVD Manager). After it has completed its load sequence, you can find your device name(s) by typing ADB devices at the command-line prompt. You should see something like emulator-xyzw. You can now navigate to the directory where you extracted the Adobe AIR SDK (AdobeAIRSDK\runtimes\air\android\emulator\Runtime.apk) and type: ADB -s emulator-xyzw install Runtime.apk where emulator-xyzw is your device name, found using the Devices command. (You can also find this in your taskbar if you’re using Windows.) To install a Flash game on your Android emulator, navigate to the directory with the game and use the same syntax, substituting yourgame.apk for Runtime.apk: ADB -s emulator-xyzw install yourgame.apk To delete an app from your emulator or stop it from running, click the Home button and click the Apps icon. Click the Menu button and click Manage Apps. Navigate to the app you want to delete or stop and click Uninstall or Force Stop. You can edit your system path in a Mac by editing the .profile file in your home directory. If there are already entries, append the additional path by separating with a full colon, not a semicolon. For additional information about ADB, check http://developer.android.com/guide/developing/tools/ADB.html. Adobe AIR Developer Tool (ADT) The command required to execute ADT and have it add the AIR runtime to my emulator-5556 is: F:Downloads\AdobeAir\AdobeAIRSDK\bin\adt -installRuntime -platform android -device emulator-5556 -package F:Downloads\AdobeAir\AdobeAIRSDK\runtimes\air\android\emulator\ Runtime.apk That is clearly a ridiculous amount of typing with far too many opportunities for typos. If you navigate to your AdobeAIRSDK directory in the command-line interpreter, you can use shorter local paths:
Testing an Android Game \bin\adt -installRuntime -platform android -device emulator-5556 -package \runtimes \air\android\emulator\Runtime.apk Your local paths will look exactly the same, though the location of your AdobeAIRSDK directory will be different from mine. But it’s still too much tedious typing, especially because you have to navigate to the AdobeAIRSDK directory before you can use that shorter command. So, the benefit of adding the path to adt.bat is significant if you’re going to use ADT to install the runtime and games onto Android emulators. To add the ADT path to your system path in Windows, click Start and right-click My Computer. Click Properties > Advanced System Settings > Environment Variables. In the System Variables field, click Path > Edit. Click the arrow to the last entry of the Variable value line. Append a semicolon and type the full path to your AdobeAir\AdobeAIRSDK\bin directory. Finally, click OK. You can now type adt from anywhere in your command-line editor, and ADT in the AdobeAIRSDK\bin directory will execute. So, to install the Adobe AIR runtime on one of your emulators, start an emulator (using AVD Manager). After it has completed its load sequence, you can find your device name(s) by executing: adt devices at the command-line prompt from the C:\Program Files (x86)\Android\android-sdk\ platform-tools\directory or from anywhere if you add C:\Program Files (x86) \Android\android-sdk\platform-tools\ to your system path. You can now navigate to the directory where you extracted the Adobe AIR SDK (AdobeAIRSDK\runtimes\air\android\emulator\Runtime.apk) and type: adt -installRuntime -platform android -device emulator-xyzw -package Runtime.apk where emulator-xyzw is your device name, found using the Devices command. (You can also find it in your taskbar if you are using Windows.) To install a Flash game on your Android emulator, navigate to the directory with the .apk game file and use: adt -installApp -platform android -device emulator-xyzw -package yourgame.apk where emulator-xyzw is your device name (for example, emulator-5554), found using the Devices command (you can also find it in your taskbar if you’re using Windows) and yourgame.apk is the file name of your Android game. 373
374 Chapter 9 n Developing and Distributing Games for Android Devices To delete an app from your emulator or stop it from running, click the Home button and then the Apps icon. Click the Menu button and then click Manage Apps. Navigate to the app you want to delete or stop and then click Uninstall or Force Stop. You can edit your system path in a Mac by editing the .profile file in your home directory. If there are already entries, append the additional path by separating with a full colon. For additional information about ADT, check http://help.adobe.com/en_US/air/build/WS5b3ccc516d4fbf351e63e3d118666ade467fd9.html. Publishing Your Game for Android Publishing for Android is much easier than publishing for iOS. There is no labyrinth of steps like those mandated by Apple. Furthermore, the marked contrast between Apple and everyone else extends to uploading your finished game to Google Play (the current version of Android Market) and the Amazon App Store, for example. I will provide the links for registration and uploading games in the next “Distributing Your Game for Android” section. For now, let’s continue with the publishing dialogue. AIR for Android Settings: General Tab Open Flash and create a new FLA or load an FLA (such as switcher5.fla). Click File > Publish Settings > SWF and select the most recent version of AIR for Android in the Target combobox. To the right of the Target combobox is the Player Settings icon. Click it to open the Player Settings panel. If you selected AIR for Android in the Target combobox, this panel should be the AIR for Android Settings panel. The General tab should be selected by default. If not, click it. (See Figure 9.3.)
Publishing Your Game for Android Figure 9.3 The AIR for Android Settings panel with the General tab selected. Source: Adobe Systems Incorporated. The Output File value is the name of the .apk file that Flash will publish. It is the file that will be installed on the devices and/or emulators for testing. The App Name field contains the name of your game, which will appear under your game’s icon when installed on an Android device (and in the app stores). A limited number of characters will display. The Version value doesn’t need to change unless you’re updating a previous game version. The Aspect ratio should be Auto if you want your game to rotate when the device is rotated. Otherwise, select Portrait or Landscape. 375
376 Chapter 9 n Developing and Distributing Games for Android Devices Check Full screen and/or Auto orientation if desired. You should check Auto orientation if you selected Auto in the Aspect ratio combobox. Render mode is probably the most important option in this panel. What you select there can make the difference between a poorly performing game and a game with smooth play. There are some guidelines for what you should select, but none of them is worthwhile, except that if you’re using Stage3D API, you must select Direct. Otherwise, you should test with both CPU and GPU to see which is best. Generally, if you’re doing bitmap manipulation, GPU will be better. But I don’t think that’s helpful. You won’t care whether performance is usually better with GPU render mode. You want to know if performance is better with GPU mode. And that is best determined by testing the performance with both CPU and then GPU modes on the target (not emulator) platform. Again, if you are using Stage3D choose Render mode Direct. No other option will work with Stage3D. If you are not using Stage3D, choose Render mode CPU and test on your target Android device. Then choose Render mode GPU and test on your target Android device. Use the Render mode that worked best when you tested. There is also an Auto render mode, but I believe that’s still the same as CPU mode. Eventually, Adobe may implement (or abandon) some sort of Auto render mode, but as of the time of this writing, Auto and CPU mode are the same. And lastly, the Included Files section of this panel should contain two files by default: the published SWF and an XML file called the application descriptor. The application descriptor contains the information from the AIR for Android Settings panel that you’re currently editing. In addition, you can (and should) add any files needed by your game. None of your class files needs to be added, because all that code is compiled into your SWF by Flash when you publish the SWF. But if you load bitmaps, data files, or another SWF, you should add them to this section. (Another difference between Flash-made iOS and Android apps/games is you can load SWFs into an Android app/game, and code in the loaded SWF will execute.) You can click the icon on the left to add individual files to the included file list, and you can click the icon on the right to add entire directories of files. The middle icon is used to remove something you previously added that is no longer needed.
Publishing Your Game for Android AIR for Android Settings: Deployment Tab Click the Deployment tab. Here is where you will create your developer Certificate (See Figure 9.4). Figure 9.4 AIR for Android Settings panel with Deployment tab selected. Source: Adobe Systems Incorporated. To the right of the Certificate field is a Create button that you can click to create a self-signed digital Certificate (see Figure 9.5). 377
378 Chapter 9 n Developing and Distributing Games for Android Devices Figure 9.5 Create Self-Signed Digital Certificate panel. Source: Adobe Systems Incorporated. Fill in the fields using your name in the first three fields unless you’re part of an organization, in which case you can fill in your name, your department, and your organization’s name in the first three fields. Enter and confirm a password that you will use every time you publish using this Certificate. Leave the encryption type at 1024-RSA (unless you know you need stronger encryption), leave the validity period at 25 years (or increase it), and save your Certificate using a name that indicates it is an Android Certificate. If you already have a Certificate, you need not create another one. Click the Browse button instead of the Create button and navigate to your Certificate. If you want or need to create a Certificate issued by a certification authority, be prepared to pay a few hundred dollars per year. You can use VeriSign, Thawte, GlobalSign, or ChosenSecurity. Cost and exact steps to create and download a verified Certificate vary. You can get detailed help about using a certification authority from Adobe at http://help.adobe.com/en_US/air/build/WS5b3ccc516d4fbf351e63e3d118666ade467ff0.html.
Publishing Your Game for Android Next, enter the password associated with your Certificate and check Remember Password for this session unless you want to re-enter the password every time you publish during your current session. Select one of the Android deployment option types. 1. Device release 2. Emulator release 3. Debug Option 1 Use Option 1 for testing on an Android device. If your Android device is connected to your development platform when you publish or an Android emulator is running, Flash will install your game on your device/emulator if you check Install Application on the Connected Android Device toward the bottom of this panel. If your Android device is not recognized, you may need to install a USB driver for Android. Open the Device Manager to see whether there is a driver issue. If there is, you can manually install a driver from the AirX.Y/install/android/usb_drivers subdirectory of your Flash Pro installation directory. Option 2 If you have a version of Flash Pro that lets you select Emulator Release and that allows you to select Embed AIR Runtime with Application, you could use this option to test in the Android emulator. In my Flash Pro CS6, selecting Emulator Release prohibits embedding the AIR runtime, which means you cannot use the emulator release in the Android emulator, and that makes the Emulator Release option useless. However, if you can embed the AIR runtime with the Emulator release, you can install and use this release in the Android emulator. To install the published .apk file manually (not recommended if an emulator is running at the time you publish when you can direct Flash to install it), start your emulator (using your shortcut to the AVD Manager) and then, in the command-line interpreter, navigate to the directory with the .apk game file and use: adt -installApp -platform android -device emulator-xyzw -package yourgame.apk where emulator-xyzw is your device name (for example, emulator-5554), found using the Devices command (and can also be found in your taskbar if you’re using Windows), and yourgame.apk is the file name of your Android game. 379
380 Chapter 9 n Developing and Distributing Games for Android Devices Your game should install on that emulator. If your emulator’s APPS screen is filled, you may need to navigate to the bottom right (using your keyboard arrow keys or the emulator’s arrow keys) and then click the right arrow to see more installed applications. Option 3 If you want to test on an Android device and see trace output in Flash, select Option 3. Hopefully, you will see a network interface for remote debugging displayed in the combobox. Copy the IP address if that is what you’re going to select. After selecting a network interface, publish your .apk game file. Follow the same steps listed in Option 1 to load your game onto your Android device. (You won’t be able to use an emulator with remote debugging unless you can embed the AIR runtime.) In Flash, click Debug > Begin Remote Debug Session > ActionScript 3.0. Tap your newly loaded game. It may take a minute to start up. When it does, you should see a Flash Debugger prompt for the IP address or hostname that you just copied. Type the IP address or hostname and tap OK. AIR for Android Settings: Icons Tab Using this tab, you can add icons for your game when it’s installed on your target device. You can create these icons using Flash (or your preferred graphics program). To use Flash, add a MovieClip (such as switch), Button, or Graphic to the stage, size it so the largest dimension (width or height) matches the icon size, right-click it, and click Export PNG Sequence. If you used a multiframe object, pick the PNG that you want to use for the icon and delete the rest. Add all the icon sizes used by your target device. AIR for Android Settings: Permissions Tab Check the permissions needed by your game, if there are any. Users installing your game will see a warning notifying them that your game is seeking access to features related to privacy or security. If your game needs a permission and you fail to request it, your game—or at least the part of your game that needs that permission—will fail. If your game needs no permissions, you will see a warning from Flash when you publish. You can ignore that warning (See Figure 9.6).
Publishing Your Game for Android Figure 9.6 Expected Flash warning when no permissions are requested. Source: Adobe Systems Incorporated. AIR for Android Settings: Languages Tab If you’re using Flash Pro CS6 or better, you will have a Languages tab in your AIR for Android Settings panel. Otherwise, you won’t, and you can ignore this section. The panel displayed by selecting this tab allows you to indicate other languages your game supports. That information is added to your application descriptor and will be displayed by the Google Play Store. 381
382 Chapter 9 n Developing and Distributing Games for Android Devices It is important to note that no language support is added to your game no matter what languages you check in this panel. Actually, adding language support is up to you, the developer. Distributing Your Game for Android Here is where you can get some perspective on the difficulty imposed by Apple for distributing games. For everyone other than Apple, there is essentially nothing to say about how to distribute your game. Just navigate to the links and follow the directions. n https://play.google.com/apps/publish/signup. This link is to Google Play (formerly Android Market), where you are charged $25 (which I believe is a one-time charge, not per year). n https://developer.amazon.com/welcome.html. This link is to Amazon App Store for Android, where you are charged $99 per year (but the first year is free—at least at the time of this writing). Of course, if you sell your game or anything in your game, each store will take a cut. Check their terms if that matters to you.
Chapter 10 3D Game Development In this chapter, I’ll discuss how to get started with developing a 3D game using Flash. Thanks to major improvements with graphics handling introduced with Flash Player 11, there has been an increased interest in Flash 3D games. The big change with Flash Player 11 was the addition of Stage3D rendering to Flash games. Stage3D rendering allows you to leverage GPU acceleration that should dramatically improve the performance of games that use it. At the time of this writing (Flash Player 11.2), there is still room for improvement with some Flash Players (especially debug versions) in some browsers. Hopefully, by the time this book is published, the latest Flash Player version will offer a major boost to Stage3D games across all platforms with both debug and non-debug versions. The Stage3D APIs offer low-level access to encode highly efficient GPU acceleration. However, the Stage3D APIs are so complex to use that they aren’t suitable for this book’s audience. Fortunately, several available frameworks provide a buffer between the Stage3D APIs and the developer, allowing the use of higher-level APIs. Among those frameworks is Starling, a 2D framework encountered in Chapter 7, “Optimizing Game Performance,” and 3D frameworks Alternativa3D, Away3D, Flare3D, Proscenium, and more. 383
384 Chapter 10 n 3D Game Development One of the easiest to use is Flare3D (www.flare3d.com). For noncommercial use, it’s free. However, it is not open source, and it costs $496 per year for commercial use, so if those are issues for you, you may want to skip the next section. If you need information about the basics of 3D graphics, check http://computer .howstuffworks.com/3dgraphics.htm. If you need background definitions of the terms used in 3D programming, check http://jmonkeyengine.org/wiki/doku.php/jme3:terminology. Even though that link is related to a Java programming engine, the terms used are common to all 3D programming. Flare3D To start using Flare3D, click the Download Now button from their main page (www. flare3d.com). Then click the Download Trial button for the noncommercial version of Flare3D. After you fill in their form, a link to the download package will be emailed to the address you supplied in the form. You will download Flare3D.zip, which, when unzipped, will create some files and five directories. The lib and docs directories contain the files we will use in this section. The lib directory contains a .swc file that contains the compiled Flare3D code. You will need to add that SWC file to your library path. Click File > Publish Settings > ActionScript Settings icon (see Figure 10.1). Click the Library Path tab and the Browse to SWC File icon (see Figure 10.2). Finally, navigate to the Flare3D SWC file and click Open, then click OK, and then OK again.
Flare3D The ActionScript Settings icon Figure 10.1 Click File > Publish Settings to view the Publish Settings panel. Source: Adobe Systems Incorporated. 385
386 Chapter 10 n 3D Game Development The Browse to SWC file icon Figure 10.2 After clicking the ActionScript Settings icon, you will see the Advanced ActionScript 3.0 Settings panel. Source: Adobe Systems Incorporated. The docs directory contains the Flare3D API. The API is incomplete, but it’s still helpful. I needed to supplement the information in the API with experimentation and Google searches. To open the API, navigate to the docs directory and open index.html in your browser. You should see a document that is laid out in the familiar format of the Flash API, with packages listed in an upper-left panel and classes listed in a lowerleft panel. I’m going to demonstrate some Flare3D basics using a game (Yellow Planet) I downloaded from the Flare3D website. Open/support files/Chapter 10/yp/yp.fla in Flash
Version 01 Pro. You can remove the path to the Flare3D SWC file that I used and add the path to the Flare3D SWC that you downloaded. Then, in /support files/Chapter 10/yp/com/kglad, open the class files Main, IntroView, GameOverView, Data, and pseudo class GameView_01. Rename GameView_01 to GameView. All of these files except GameView should look very familiar. I’m not going to say anything about any of the classes except GameView, which is the only class that uses the Flare3D API and is the only class with code that hasn’t been covered previously. All of the classes other than GameView will remain unchanged through all the game versions in this section, so they aren’t listed with a version number. GameView contains extensive comments to help you understand how Flare3D works. You should have the following…. Version 01 Main package com.kglad { import flash.display.Sprite; import flash.events.Event; import flash.display.StageAlign; import flash.display.StageScaleMode; public class Main extends Sprite { private var introView:Sprite = new IntroView(); private var gameView:Sprite = new GameView(); private var gameOverView:Sprite = new GameOverView(); public function Main() { //MT.init(this,2); stage.scaleMode = StageScaleMode.NO_SCALE; stage.align = StageAlign.TOP_LEFT; addIntroViewF(); } private function addIntroViewF():void{ addChild(introView); introView.addEventListener(“startGameE”,startGameF,false,0,true); } private function startGameF(e:Event):void{ removeIntroViewF(); addGameViewF(); } private function removeIntroViewF():void{ removeChild(introView); introView.removeEventListener(“startGameE”,startGameF,false); 387
388 Chapter 10 n 3D Game Development } private function addGameViewF():void{ addChild(gameView); gameView.addEventListener(“gameOverE”,gameOverF,false,0,true); } private function gameOverF(e:Event):void{ removeGameViewF(); addGameOverViewF(); } private function removeGameViewF():void{ removeChild(gameView); gameView.removeEventListener(“gameOverE”,gameOverF,false); } private function addGameOverViewF():void{ addChild(gameOverView); gameOverView.addEventListener(“replayE”,replayF,false,0,true); } private function replayF(e:Event):void{ removeGameOverViewF(); addIntroViewF(); } private function removeGameOverViewF():void{ removeChild(gameOverView); gameOverView.removeEventListener(“replayE”,replayF,false); } } } IntroView package com.kglad { import flash.display.Sprite; import flash.events.Event; import flash.events.KeyboardEvent; public class IntroView extends Sprite{ private var ic:InstructionsControls = new InstructionsControls(); private var startGameE:Event = new Event(“startGameE“); public function IntroView() { this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanupF,false,0,true); } private function init(e:Event):void{ this.addChild(ic);
Version 01 stage.addEventListener(KeyboardEvent.KEY_DOWN,keydownF,false,0,true); } private function keydownF(e:KeyboardEvent):void{ if(e.keyCode==32){ dispatchEvent(startGameE); } } private function cleanupF(e:Event):void{ stage.removeEventListener(KeyboardEvent.KEY_DOWN,keydownF,false); } } } Version_01 GameView package com.kglad { import flare.basic.Scene3D; import flare.basic.Viewer3D; import flare.core.Pivot3D; import flare.loaders.Flare3DLoader1; import flash.display.*; import flash.events.*; public class GameView extends Sprite { // a Scene3D instance is the first object needed to // create a 3d scene. private var scene:Scene3D; // planet is a 3d model that will be imported // (models/planet.f3d) private var planet:Pivot3D; public function GameView() { // The usual this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanupF,false,0,true); } private function init(e:Event):void{ // The first time the GameView instance is // added to the stage, this branch executes. // Thereafter, the else branch executes. if(!scene){ // For this step, instead of using a Scene3D // instance, use a Viewer3D instance. Viewer3D // is a Scene3D subclass with some extra // functionality. Namely, you can use your 389
390 Chapter 10 n 3D Game Development // mouse to drag-rotate and scrollwheel-zoom. //scene = new Scene3D(Sprite(root)); scene = new Viewer3D(Sprite(root)); // Check Scene3D in the API. Without this // statement a default logo appears in the // four corners of the stage. //scene.showLogo = false; // This prevents scene from updating, which // at this point means this prevents scene // from starting to render. When // scene.resume() executes, scene resumes // updating. scene.pause(); // Check the API if you are unsure what // this does. scene.antialias = 1024; // The Scene3D.PROGRESS_EVENT will be used // in the next version to display a preloader. scene.addEventListener( Scene3D.PROGRESS_EVENT, progressF ); // The Scene3D.COMPLETE_EVENT is used to indicate // when the scene is ready to render (and then // start the game action). scene.addEventListener(Scene3D.COMPLETE_EVENT, completeF ); // This is needed when using the Scene3D method // addChildFromFile(), which adds an object from // a file, or you can use just Flare3DLoader1 on // a line by itself. scene.registerClass( Flare3DLoader1); // // // // // // // // // // // // // This loads the planet model (models/planet.f3d) from an external file and stores a reference in a Pivot3D instance. The Pivot3D class is the most basic 3D element in Flare. The f3d file format is a Flare3D file format (per the Flare website), but it started as an open source file format for the storage of volumetric data. Flare3D can also import Collada and Obj files, so you can use 3D Max, Maya, Blender, and Art of Illusion to create 3D models. Blender and Art of Illusion are free, with Art of Illusion being easier to learn but without all the
Version 01 // features of Blender. planet = scene.addChildFromFile( “models/planet.f3d” ); } else { // Everything is already loaded, so no need // for a progress or complete event. startGame(); scene.resume(); } } private function progressF(e:Event):void { // Nothing here yet, but scene.loadProgress returns // a number between 0 and 100 representing the // percentage of the scene that is loaded. } private function completeF(e:Event):void { // startGame() will be used to set up some of // the game elements. startGame(); // Render the scene. scene.resume(); } private function startGame():void { // To be done. } private function cleanupF(e:Event):void{ // Again, this prevents the scene from updating. // There’s no need to use CPU/GPU cycles when // the GameView instance is removed from the // stage. (Though scene will still be visible.) scene.pause(); } } } GameOverView package com.kglad { import flash.display.Sprite import flash.events.Event; import flash.events.KeyboardEvent; public class GameOverView extends Sprite{ private var scoreDisplay:Score = new Score(); private var replayE:Event = new Event(“replayE“); public function GameOverView() { this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); 391
392 Chapter 10 n 3D Game Development this.addEventListener(Event.REMOVED_FROM_STAGE,cleanupF,false,0,true); } private function init(e:Event):void{ stage.addEventListener(KeyboardEvent.KEY_DOWN,keydownF,false,0,true); this.addChild(scoreDisplay); scoreDisplay.tf.text = Data.score.toString(); scoreDisplay.best.tf.text = Data.bestScore.toString(); } private function keydownF(e:KeyboardEvent):void{ if(e.keyCode==32){ dispatchEvent(replayE); } } private function cleanupF(e:Event):void{ stage.removeEventListener(KeyboardEvent.KEY_DOWN,keydownF, false); } } } Data package com.kglad { public class Data { private static var _score:int; private static var _bestScore:int; public static function get score():int{ return _score; } public static function get bestScore():int{ return _bestScore; } public static function set score(n:int):void{ _score = n; if(_score>_bestScore){ _bestScore = n; } } } }
Version 02 Test that to see the initial game. Use your mouse to rotate and zoom the planet. There’s nothing else encoded, so checking the planet is all you can do with that first version, and that planet defines the extent of the 3D world for this game. In general, with 3D games you will need to initialize the world and the objects in the world. You will also need to initialize and control a camera. Further, you will probably have an object (or objects) that move through your 3D world. All the objects and the camera in your 3D world will need to have a 3D position (or a location in the world) and a 3D rotation (or orientation in the world). That is, they will all, at a minimum, need a position and need to face some direction in order to define their appearance to the viewer. There are other things you could consider adding to a 3D game, such as a view (how the 3D world is projected onto a 2D plane), lights, and shadows, but I won’t discuss those. In this next version of GameView, we’re going to add the camera, an astro character that can be moved through the 3D world (using the keyboard arrow keys), and an energy 3D model, and we’ll animate the fans and mines that are part of the planet model. We will also add the preloader display. Version 02 Version_02 GameView package com.kglad { import flare.basic.Scene3D; import flare.basic.Viewer3D; import flare.loaders.Flare3DLoader1 import flare.core.*; import flare.utils.*; import flare.system.Input3D; import flare.collisions.* import flash.display.*; import flash.events.*; import flash.geom.*; public class GameView extends Sprite { private var scene:Scene3D; private var planet:Pivot3D; private var astro:Pivot3D; private var astroP:Pivot3D; private var shadow:Pivot3D; private var sky:Pivot3D; private var energy:Pivot3D; 393
394 Chapter 10 n 3D Game Development private var mineA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var fanA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var pointA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var mine:Pivot3D; private var fan:Pivot3D; private var i:int; private var rayCollision:RayCollision; private var sphereCollision:SphereCollision; private var astroAbove:Vector3D = new Vector3D(0,100,0); private var astroFrom:Vector3D = new Vector3D(); private var astroDown:Vector3D; private var info:CollisionInfo; private var startPoint:Pivot3D; private var energyIndex:int; private var astroPosition:Vector3D; // Initialize the 2D preloader display. The Loading class // object is in the yp.fla library. private var loading:Loading = new Loading(); public function GameView() { this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanupF,false,0,true); } private function init(e:Event):void{ if(!scene){ // Standard code to add the preloader to // the display addChild( loading ); scene = new Scene3D(Sprite(root)); scene.showLogo = false; scene.pause(); scene.antialias = 1024; // Create a 3D camera. Code to control // the camera’s location and orientation // are below. scene.camera = new Camera3D(); scene.addEventListener(Scene3D.PROGRESS_EVENT, progressF ); scene.addEventListener( Scene3D.COMPLETE_EVENT, completeF ); scene.registerClass( Flare3DLoader1); // Loads the external files and stores the // references into planet and astro objects. planet = scene.addChildFromFile( “models/planet.f3d” );
Version 02 astro = scene.addChildFromFile( “models/astronaut.f3d” ); // Loads a (pseudo) shadow model for astro // and an energy boost model. shadow = scene.addChildFromFile( “models/shadow.f3d” ); energy = scene.addChildFromFile( “models/energy.f3d” ); } else { startGame(); scene.resume(); // The Scene3D.UPDATE_EVENT is the Flare // equivalent of Event.ENTER_FRAME and is // an essential part of animating a Flare game. // You should not use Event.ENTER_FRAME to // animate a Flare game, because your // Event.ENTER_FRAME loop will almost // certainly be out of sync with your 3D // scene. At a minimum, if you used your // own loop to animate, you would need // to disable scene.enableUpdateAndRender // until scene.context returned true. // Then you would need to direct Flare to // update your scene animations, clear a // back buffer, render the scene, and // finally present the results. scene.addEventListener( Scene3D.UPDATE_EVENT, updateF ); } } private function progressF(e:Event):void { // Here is the code to display loading progress // of the external files loading.bar.gotoAndStop( int(scene.loadProgress) ); } private function completeF(e:Event):void { // Remove the preloader. It will not be needed again, // so it can be nulled. removeChild(loading); loading = null; // This creates an empty Pivot3D object to be used // as the astro parent. This is used later to control // scene.camera astroP = new Pivot3D(); // Add astro and his shadow to the parent. astroP.addChild(astro); astroP.addChild(shadow); 395
396 Chapter 10 n 3D Game Development // Add astroP to the scene. scene.addChild(astroP); // planet has a number of Pivot3D children. The // getChildByName() method can be used to obtain // references to them. sky = planet.getChildByName( “sky” ); // A RayCollision instance is used to create // a virtual (i.e., nothing is displayed) line // that can be used to determine // whether and where an object moving in a // straight line will impact another object. // In this game, it is used to determine where // astro should be positioned so he appears to // be rooted to the planet surface (or // floor). A ray extending from above astro’s // head through his feet will impact the planet // floor. The x, y, and z properties // of that impact point are used to assign // astro’s x, y, and z properties. rayCollision = new RayCollision(); // The second parameter in addCollisionWith() // indicates whether children are included in // the collision detection. rayCollision.addCollisionWith( planet.getChildByName( “floor” ), false ); // The forEach() method calls a function for each // of the children of the planet. planet.forEach(planetF); // Starts the level. startGame(); // Continues rendering the scene. scene.resume(); // This important line of code was discussed above. scene.addEventListener( Scene3D.UPDATE_EVENT, updateF ); } private function planetF(p3d:Pivot3D):void{ // Here, fanA, mineA, and pointA vectors are populated // with object references that will be used when // updating the scene. if(p3d.name.indexOf(“fan“)>-1){ // Pivot3D instances have a userData property // that has type Object. So, it can be used to // store an unlimited amount of data needed // by the instance.
Version 02 // randomF() is below p3d.userData = {speed:randomF(5,10)}; fanA.push(p3d); } else if(p3d.name.indexOf(“mine“)>-1){ p3d.userData = {speed:randomF(-20,-10)}; mineA.push(p3d); } else if (p3d.name.indexOf(“point“)>-1 ){ // points to position energy pointA.push(p3d); } } private function startGame():void { // startPoint is just an empty Pivot3D instance // on the planet floor used to initially // position astro. startPoint = planet.getChildByName( “start” ); // The Pivot3D copyTransformFrom() method is a // quick way to copy the position and orientation // of the argument (startPoint) to astroP. astroP.copyTransformFrom( startPoint ); // astro is an animated 3D model with frames. // Frame 6 (0-based) appears close to standing still. astro.gotoAndStop(6); // (Re)set some values. astro.y = 0; // randomize positions of energy boosts. shuffle(pointA); // position & orient energy instance energy.copyTransformFrom( pointA[energyIndex] ); } // updateF() is the game loop listener function. private function updateF(e:Event):void { // Update astro’s position and orientation. astroF(); // Update the objects (mines and fans) worldObjectsF(); // Update the camera position and orientation. cameraF(); } private function astroF():void{ // astroAbove (initialized above and // = new Vector3D(0,100,0) )is // a vector3D instance that is 100 px // above astro (in local coordinates) 397
398 Chapter 10 n 3D Game Development // astroFrom is a Vector3D instance in // global coordinates that will be // rayCollisions start point astroFrom= astroP.localToGlobal(astroAbove); // Pivot3D instances have getLeft(), getRight(), // getUp(), and getDown() methods that return the // Vector3D direction to the left, right, up, and // down of the Pivot3D instance. (There is also a // getDir() method that returns the direction to // the front of the Pivot3D instance.) // Here we want the down direction to use with // rayCollision that is starting just above astro // and will extend down through his // feet (and onwards). Where that ray collides with // the planet floor is where astro should be // positioned. astroDown = astroP.getDown(); // Test the ray using a “from” Vector3D and a // “direction” Vector3D if (rayCollision.test(astroFrom, astroDown)) { // The data Vector (like an Array, not a // Vector3D) contains CollisionInfo elements // with information about each // object with a positive collision. // If the addCollisionWith() second parameter // is true, there may be more than one object // in data. info = rayCollision.data[0]; // Root astroP at the collision point. astroP.setPosition(info.point.x, info.point.y, info.point.z ); // Align the astroP to the collision // normal (= perpendicular) so astro appears to // be standing upright (and not leaning or upside // down). The second parameter in // setNormalOrientation() is an easing parameter // (zero to one with default = 1). astroP.setNormalOrientation( info.normal, 0.05 ); } // // // // // Flare3D has an Input3D class with static methods that facilitate quick coding for detecting keyboard input. rotateY() is a Pivot3D method that accepts three parameters. The first is the amount (in degrees) to rotate.
Version 02 // // // // // // // if The second is a Boolean indicating whether to use local coordinates (default = true), and the third is a Vector3D pivot point (default = null). Of course, there are also rotateX and rotateZ methods. Notice these methods work differently from the Flash rotationX, rotation, and rotationZ properties. ( Input3D.keyDown( Input3D.RIGHT ) ) { astroP.rotateY( 2 ); } if ( Input3D.keyDown( Input3D.LEFT ) ) { astroP.rotateY( -2 ); } if ( Input3D.keyDown( Input3D.UP ) ) { astroP.translateZ( 1 ); } if ( Input3D.keyDown( Input3D.DOWN ) ) { astroP.translateZ( -1 ); } } // Update the world objects. private function worldObjectsF():void{ // The getPosition() Pivot3D method returns a position // Vector3D instance. It accepts two parameters. The // first indicates whether the returned Vector3D is // in local coordinates (default = true), and the second // accepts a Vector3D if you want to assign the return // value to that second parameter. The default is null. // This is an example where I found the API lacking. // It is not clear to me when or why a second parameter // should be used. astroPosition = astro.getPosition(false,null); // astroPosition is used to determine when astro is // close to a fan or mine, which will be used in // another version. for(i=fanA.length-1;i>=0;i--){ fan = fanA[i]; fan.rotateY( fan.userData.speed, true, null ); } for(i=mineA.length-1;i>=0;i--){ mine = mineA[i]; mine.rotateY(mine.userData.speed); } // This adds a little ambiance. 399
400 Chapter 10 n 3D Game Development sky.rotateX(0.1); } private function cameraF():void{ // The Pivot3DUtils class has two static methods used // here to link the position and orientation of an // object (scene.camera in the case) with another object // (astroP in this case). // The first method is used to position the camera (0px) // to the right, (40px) above and (30px) behind astroP. // The last parameter in // Pivot3DUtils.setPositionWithReference() is an easing // parameter between 0 and 1 with default = 1. Pivot3DUtils.setPositionWithReference(scene.camera, 0, 40, -30, astroP, 0.1); // The second method is used to orient (or rotate) // the camera toward the front of astroP. // Again, second, third, and fourth parameters are // the x, y, and z offsets relative to astroP. The // last two parameters are the up direction of // scene.camera and an easing parameter. Pivot3DUtils.lookAtWithReference(scene.camera, 0, 0, 0, astroP, astroP.getUp(), 0.2 ); } private function cleanupF(e:Event):void{ scene.pause(); scene.removeEventListener( Scene3D.UPDATE_EVENT, updateF ); } private function shuffle(a:Vector.<Pivot3D>) { var i:int; var j:int; var e:*; var len:int = a.length; for (i = len-1; i>=0; i--) { j=Math.floor((i+1)*Math.random()); e = a[i]; a[i] = a[j]; a[j] = e; } } private function randomF(n1:Number,n2:Number):Number{ if(n1>n2){ return n2+(n1-n2)*Math.random(); } else { return n1+(n2-n1)*Math.random();
Version 03 } } } } Test that and explore the world. Explore the area around a larger fan’s perimeter. Notice how astro stays perpendicular to the planet surface as you move in and out of that perimeter. But also notice how astro can leave the planet surface when you orient him such that rayCollision fails to intersect the planet. That won’t be a problem in the next version because we’re going to add collision detection with objects so that astro won’t be able to walk through objects. Version 03 Version_03 GameView package com.kglad { import flare.basic.Scene3D; import flare.basic.Viewer3D; import flare.loaders.Flare3DLoader1 import flare.core.*; import flare.utils.*; import flare.system.Input3D; import flare.collisions.* import flash.display.*; import flash.events.*; import flash.geom.*; public class GameView extends Sprite { private var scene:Scene3D; private var planet:Pivot3D; private var astro:Pivot3D; private var astroP:Pivot3D; private var shadow:Pivot3D; private var sky:Pivot3D; private var energy:Pivot3D; private var mineA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var fanA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var pointA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var mine:Pivot3D; private var fan:Pivot3D; private var i:int; private var rayCollision:RayCollision; private var sphereCollision:SphereCollision; private var astroAbove:Vector3D = new Vector3D(0,100,0); private var astroFrom:Vector3D = new Vector3D(); 401
402 Chapter 10 n 3D Game Development private private private private var var var var astroDown:Vector3D; mineDown:Vector3D; info:CollisionInfo startPoint:Pivot3D; private var state:String = “run“; private var speed:Number; private var level:Number; private var jumpValue:Number; private var running:Boolean; private var energyCount:int; private var energyIndex:int; private var shakeFactor:Number; private var resetCounter:int; private var radius:Number; private var astroPosition:Vector3D; private var loading:Loading = new Loading(); private var gameOverE:Event = new Event(“gameOverE“); public function GameView() { this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanupF,false,0,true); } private function init(e:Event):void{ speed = 0.75; jumpValue = 0; energyCount = 0; if(!scene){ addChild( loading ); scene = new Scene3D(Sprite(root)); scene.showLogo = false; scene.pause(); scene.antialias = 1024; scene.camera = new Camera3D(); scene.addEventListener( Scene3D.PROGRESS_EVENT, progressF ); scene.addEventListener( Scene3D.COMPLETE_EVENT, completeF); scene.registerClass( Flare3DLoader1); planet = scene.addChildFromFile( “models/planet.f3d” ); astro = scene.addChildFromFile( “models/astronaut.f3d” ); shadow = scene.addChildFromFile( “models/shadow.f3d” ); energy = scene.addChildFromFile( “models/energy.f3d” ); } else { startGame();
Version 03 scene.resume(); scene.addEventListener(Scene3D.UPDATE_EVENT, updateF ); } } private function progressF(e:Event):void { loading.bar.gotoAndStop( int(scene.loadProgress) ); } private function completeF(e:Event):void { removeChild(loading); astroP = new Pivot3D(); astroP.addChild(astro); astroP.addChild(shadow); scene.addChild(astroP); sky = planet.getChildByName( “sky” ); rayCollision = new RayCollision(); rayCollision.addCollisionWith( planet.getChildByName( “floor” ), false ); // The SphereCollision() constructor accepts // three parameters. The first (source) is a // Pivot3D instance, which will have a virtual // sphere around it to detect collisions with // other world objects. The second is a Number // (default = 1), the radius of the virtual // sphere, and the third is a local (to the first // parameter) Vector3D offset from the source’s // 0,0,0 point. In this example, astroP is the // source and 0,0,0 of astro (the only object // in astroP) is at his feet, so an offset of // (0,3,0) will move the virtual sphere’s center // to astro’s center. sphereCollision = new SphereCollision( astroP, 3, new Vector3D( 0, 3, 0 ) ); planet.forEach(planetF); startGame(); scene.resume(); scene.addEventListener( Scene3D.UPDATE_EVENT, updateF ); } private function planetF(p3d:Pivot3D):void{ if(p3d.name.indexOf(“fan“)>-1){ p3d.userData = {speed:randomF(5,10)}; fanA.push(p3d); } else if(p3d.name.indexOf(“mine“)>-1){ p3d.userData = {speed:randomF(-20,-10)}; 403
404 Chapter 10 n 3D Game Development mineA.push(p3d); } else if (p3d.name.indexOf(“obstacle“)>-1 ){ // The SphereCollision class has an // addCollisionWith() method that allows you // to add Pivot3D instances to be used for // collision testing. In this case, if the // object is an obstacle, it is added to the // collection of objects to be used for // collision testing. The second parameter // (default = true) indicates whether // Pivot3D children are used for testing. sphereCollision.addCollisionWith(p3d, false ); } else if (p3d.name.indexOf(“point“)>-1 ){ pointA.push(p3d); } } private function startGame():void { startPoint = planet.getChildByName( “start” ); astroP.copyTransformFrom( startPoint ); astro.gotoAndStop(6); // astroP will be made not visible when he dies. astroP.visible = true; // A state variable will be used in astroF() to // determine what astro is doing. state = “run“; // A running variable used in astroF() to determine // when astro starts running. running = false; // shakeFactor is used to “shake” the camera when // astro jumps and collides with fans and mines shakeFactor = 0; shuffle(pointA); energy.copyTransformFrom( pointA[energyIndex] ); // The SphereCollision class uses the previous position // and trajectory of the source to determine collisions. // If either were reassigned, the reset() method should // be applied to sphereCollision. I did not reassign // those, so the following is unneeded in this game. // sphereCollision.reset(); } private function updateF(e:Event):void { astroF(); worldObjectsF(); cameraF();
Version 03 } private function astroF():void{ astroFrom= astroP.localToGlobal(astroAbove); astroDown = astroP.getDown(); if (rayCollision.test(astroFrom, astroDown)) { info = rayCollision.data[0]; astroP.setPosition( info.point.x, info.point.y, info.point.z ); astroP.setNormalOrientation( info.normal, 0.05 ); } // When astroP moves, test for collisions. If astroP // is colliding with something, the slider() method // will yield a displacement dependent on the collision // angle and will position astroP accordingly. // Contrast slider() to the fixed() method that // positions astroP at the collision point. There is // also an intersect() method that does no // repositioning of astroP. sphereCollision.slider(); // Here is where the start variable is used to // determine what astro and astroP should do. switch( state ){ case “run“: if ( Input3D.keyDown( Input3D.RIGHT ) ) { astroP.rotateY( 2 ); if(!running){ // Here is where the variable // running is used to prevent // repeatedly executing // astro.gotoAndPlay() running = true; // The Pivot3D gotoAndPlay() // accepts three parameters. // The first parameter is // frame label or a frame // number similar to the // Flash first gotoAndPlay() // parameter. The second is // blendFrames, which is the // number (default = 0) of // frames blended when // going from one animation // to another. The third // parameter is a loop int 405
406 Chapter 10 n 3D Game Development // with default = 0 // (regular looping), 1 for // ping-pong looping, and 2 // to stop at the end of the // animation sequence. astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.RIGHT ) ) { running = false; astro.stop(); } if ( Input3D.keyDown( Input3D.LEFT ) ) { astroP.rotateY( -2 ); if(!running){ running = true; astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.LEFT ) ) { running = false; astro.stop(); } //astro.gotoAndStop( “run”,3); if ( Input3D.keyDown( Input3D.UP ) ) { astroP.translateZ( speed ); if(!running){ running = true; astro.gotoAndPlay( “run”,3,0); } } else if ( Input3D.keyUp( Input3D.UP ) ) { running = false; astro.stop(); } if ( Input3D.keyDown( Input3D.DOWN ) ) { astroP.translateZ( -speed ); if(!running){ running = true; astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.DOWN ) ) { running = false; astro.stop(); } if ( Input3D.keyHit( Input3D.SPACE ) ) { // jumpValue is used below to update
Version 03 // astro’s y property. jumpValue = 4; state = “jump“; astro.gotoAndPlay( “jump”, 3); } break; case “jump“: if ( astro.y == 0 ){ // astro’s returned to the planet // surface. state = “run“; if(!running){ astro.gotoAndStop(6); } } if ( Input3D.keyDown( Input3D.UP ) ) { astroP.translateZ( speed ); running = true; } if ( Input3D.keyDown( Input3D.DOWN ) ) { astroP.translateZ( -speed ); running = true } break; case “fan“: // astro collided with a fan jumpValue = 4; astroP.rotateY(1); shakeFactor = 1; // Because state not changed here, jumpValue // keeps resetting to 4 and astroP continues // to rotate and rise above the planet surface. // When astro rises above 500, reposition // astro at the start (and change state // to “run“). if ( astro.y > 500 ) { startGame(); } break; case “energy“: // astro collided with the energy // boost resetCounter acts like a // timer. Used here it stops astro // from moving while a level-up 407
408 Chapter 10 n 3D Game Development // message is displayed. resetCounter--; if ( resetCounter < 0 ) state = “run“; break; case “die“: // astro died. Delay dispatching the // gameOverE event. resetCounter--; if ( resetCounter < 0 ) { dispatchEvent(gameOverE); } break; } if(jumpValue!=0){ // Apply gravity to slow astro’s rise and then // accelerate his fall. jumpValue -= 0.3; // Update astro’s y property. astro.y += jumpValue; // Stop astro’s jump if ( astro.y < 0 ) { jumpValue = 0; astro.y = 0; } } } private function worldObjectsF():void{ astroPosition = astro.getPosition(false); for(i=fanA.length-1;i>=0;i--){ fan = fanA[i]; fan.rotateY( fan.userData.speed ); radius = fan.scaleX * 15; // If the distance between the astronaut and // fan is less than the fan radius, change // state to indicate a fan collision. Note if // you jump and are high enough over the // fan, this conditional will be false. if (Vector3D.distance( fan.getPosition(), astroPosition ) < radius ){ state = “fan“; } } for(i=mineA.length-1;i>=0;i--){
Version 03 mine = mineA[i]; mine.rotateY(mine.userData.speed); // After destroying a mine, it is made not // visible. So, only want to destroy it again // (or allow it to kill astro) if it is visible if ( mine.visible && Vector3D.distance( mine.getPosition(), astroPosition ) < 10 ){ if ( state == “jump” ){ // If the astro is jumping, destroy // the mine. mine.visible = false; shakeFactor = 2; } else if ( state == “run” ){ // if astro is running, kill astro astroP.visible = false; shakeFactor = 15; state = “die“; resetCounter = 120; } } // // // if Check if mine was destroyed. If it was and it is on the hemisphere opposite of astro, reactivate it (i.e., make it visible). (!mine.visible) { // The dot product of two vectors is // negative if the angle between them // is between 90 and 270 degrees. // That is, if the two vectors point // “away” from each other, their dot // product is negative. mineDown = mine.getDown(); astroDown = astroP.getDown(); if ( mineDown.dotProduct( astroDown ) < 0 ){ mine.visible = true; } } } if ( Vector3D.distance( energy.getPosition(), astroPosition ) < 10){ // Increment energyIndex and move the energy // Pivot3D to another point. energyIndex = (energyIndex+1)%pointA.length; energy.copyTransformFrom( pointA[energyIndex] ); 409
410 Chapter 10 n 3D Game Development energyCount++; } sky.rotateX(0.1); } private function cameraF():void{ // 3rd person Pivot3DUtils.setPositionWithReference(scene.camera, 0, 40, -30, astroP, 0.1); Pivot3DUtils.lookAtWithReference(scene.camera, 0, 0, 0, astroP, astroP.getUp(), 0.2 ); if ( shakeFactor > 0 ){ scene.camera.x += Math.random() * shakeFactor; scene.camera.y += Math.random() * shakeFactor; scene.camera.z += Math.random() * shakeFactor; shakeFactor *= 0.9; } } private function cleanupF(e:Event):void{ scene.pause(); scene.removeEventListener( Scene3D.UPDATE_EVENT, updateF ); } private function shuffle(a:Vector.<Pivot3D>) { var i:int; var j:int; var e:*; var len:int = a.length; for (i = len-1; i>=0; i--) { j=Math.floor((i+1)*Math.random()); e = a[i]; a[i] = a[j]; a[j] = e; } } private function randomF(n1:Number,n2:Number):Number{ if(n1>n2){ return n2+(n1-n2)*Math.random(); } else { return n1+(n2-n1)*Math.random(); } } } }
Version 04 Version 03 is nearly a complete game. We just need to add some scoring and sounds to make the game come alive. We also added scaling of astro’s shadow when he jumps and after he collides with a fan. Version 04 Version_04 GameView package com.kglad { import flare.basic.Scene3D; import flare.basic.Viewer3D; import flare.loaders.Flare3DLoader1 import flare.core.*; import flare.utils.*; import flare.system.Input3D; import flare.collisions.* import flash.display.*; import flash.events.*; import flash.geom.*; public class GameView extends Sprite { private var scene:Scene3D; private var planet:Pivot3D; private var astro:Pivot3D; private var astroP:Pivot3D; private var shadow:Pivot3D; private var sky:Pivot3D; private var energy:Pivot3D; private var mineA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var fanA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var pointA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var mine:Pivot3D; private var fan:Pivot3D; private var i:int; private var rayCollision:RayCollision; private var sphereCollision:SphereCollision; private var astroAbove:Vector3D = new Vector3D(0,100,0); private var astroFrom:Vector3D = new Vector3D(); private var astroDown:Vector3D; private var mineDown:Vector3D; private var info:CollisionInfo private var startPoint:Pivot3D; private var energyGUI:EnergyGUI; private var pointsGUI:PointsGUI; private var levelGUI:LevelGUI; 411
412 Chapter 10 n 3D Game Development private var state:String = “run“; private var speed:Number; private var level:Number; private var jumpValue:Number; private var running:Boolean; private var score:int; private var bestScore:int; private var energyCount:int; private var energyIndex:int; private var shakeFactor:Number; private var resetCounter:int; private var radius:Number; private var astroPosition:Vector3D; private var loading:Loading = new Loading(); // Create Sound instances private var sndDead:DeadSound = new DeadSound(); private var sndFan:FanSound = new FanSound(); private var sndJump:JumpSound = new JumpSound(); private var sndMine:MineSound = new MineSound(); private var sndReset:ResetSound = new ResetSound(); private var landSound:LandSound = new LandSound(); private var gameOverE:Event = new Event(“gameOverE“); public function GameView() { this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanupF,false,0,true); } private function init(e:Event):void{ speed = 0.75; jumpValue = 0; energyCount = 0; level = 0; Data.score = 0; score = 0; if(!scene){ // Initialize the user interface (power boost // display, level display and points display); initGUI(); addChild( loading ); scene = new Scene3D(Sprite(root)); scene.showLogo = false; scene.pause(); scene.antialias = 1024; scene.camera = new Camera3D();
Version 04 scene.addEventListener(Scene3D.PROGRESS_EVENT, progressF ); scene.addEventListener( Scene3D.COMPLETE_EVENT, completeF); scene.registerClass(Flare3DLoader1); planet = scene.addChildFromFile( “models/planet.f3d” ); astro = scene.addChildFromFile( “models/astronaut.f3d” ); shadow = scene.addChildFromFile( “models/shadow.f3d” ); energy = scene.addChildFromFile( “models/energy.f3d” ); } else { startGame(); scene.resume(); scene.addEventListener( Scene3D.UPDATE_EVENT, updateF ); } } private function progressF(e:Event):void { loading.bar.gotoAndStop( int(scene.loadProgress) ); } private function completeF(e:Event):void { removeChild(loading); astroP = new Pivot3D(); astroP.addChild(astro); astroP.addChild(shadow); scene.addChild(astroP); sky = planet.getChildByName( “sky” ); rayCollision = new RayCollision(); rayCollision.addCollisionWith( planet.getChildByName( “floor” ), false ); sphereCollision = new SphereCollision( astroP, 3, new Vector3D( 0, 3, 0 ) ); planet.forEach(planetF); startGame(); scene.resume(); scene.addEventListener( Scene3D.UPDATE_EVENT, updateF ); } private function planetF(p3d:Pivot3D):void{ if(p3d.name.indexOf(“fan“)>-1){ p3d.userData = {speed:randomF(5,10)}; fanA.push(p3d); } else if(p3d.name.indexOf(“mine“)>-1){ p3d.userData = {speed:randomF(-20,-10)}; mineA.push(p3d); } else if (p3d.name.indexOf(“obstacle“)>-1 ){ sphereCollision.addCollisionWith(p3d, false ); } else if (p3d.name.indexOf(“point“)>-1 ){ 413
414 Chapter 10 n 3D Game Development pointA.push(p3d); } } private function startGame():void { startPoint = planet.getChildByName( “start” ); astroP.copyTransformFrom( startPoint ); astro.gotoAndStop(6); astroP.visible = true; state = “run“; running = false; shakeFactor = 0; shuffle(pointA); energy.copyTransformFrom( pointA[energyIndex] ); } private function updateF(e:Event):void { astroF(); worldObjectsF(); cameraF(); } private function astroF():void{ astroFrom= astroP.localToGlobal(astroAbove); astroDown = astroP.getDown(); if (rayCollision.test(astroFrom, astroDown)) { info = rayCollision.data[0]; astroP.setPosition( info.point.x, info.point.y, info.point.z ); astroP.setNormalOrientation( info.normal, 0.05 ); } sphereCollision.slider(); switch( state ){ case “run“: if ( Input3D.keyDown( Input3D.RIGHT ) ) { astroP.rotateY( 2 ); if(!running){ running = true; astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.RIGHT ) ) { running = false; astro.stop(); } if ( Input3D.keyDown( Input3D.LEFT ) ) { astroP.rotateY( -2 ); if(!running){
Version 04 running = true; astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.LEFT ) ) { running = false; astro.stop(); } if ( Input3D.keyDown( Input3D.UP ) ) { astroP.translateZ( speed ); if(!running){ running = true; astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.UP ) ) { running = false; astro.stop(); } if ( Input3D.keyDown( Input3D.DOWN ) ) { astroP.translateZ( -speed ); if(!running){ running = true; astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.DOWN ) ) { running = false; astro.stop(); } if ( Input3D.keyHit( Input3D.SPACE ) ) { jumpValue = 4; state = “jump“; astro.gotoAndPlay( “jump”, 3 ); sndJump.play(); } break; case “jump“: if ( astro.y == 0 ){ state = “run“; if(!running){ astro.gotoAndStop(6); } } if ( Input3D.keyDown( Input3D.UP ) ) { astroP.translateZ( speed ); 415
416 Chapter 10 n 3D Game Development running = true; } if ( Input3D.keyDown( Input3D.DOWN ) ) { astroP.translateZ( -speed ); running = true } break; case “fan“: jumpValue = 4; astroP.rotateY(1); shakeFactor = 1; if ( astro.y > 500 ) { startGame(); } break; case “energy“: resetCounter--; if ( resetCounter < 0 ) state = “run“; break; case “die“: resetCounter--; if ( resetCounter < 0 ) { dispatchEvent(gameOverE); } // Update Data.score Data.score = score; break; } if(jumpValue!=0){ jumpValue -= 0.3; astro.y += jumpValue; // Scale the shadow. If returning to planet after // fan collision, probably start==“run” and // running=true. Else this is a regular jump. if(state==“run” && !running){ shadow.scaleX = shadow.scaleZ = Math.max(.01,(500astro.y)/500); } else { shadow.scaleX = shadow.scaleZ = Math.max(.01,(25astro.y)/25); } if ( astro.y < 0 ) { if(state==“run” && !running){ // If landing after a fan collision
Version 04 // play landSound landSound.play(); } jumpValue = 0; astro.y = 0; } } } private function worldObjectsF():void{ astroPosition = astro.getPosition(false); for(i=fanA.length-1;i>=0;i--){ fan = fanA[i]; fan.rotateY( fan.userData.speed ); radius = fan.scaleX * 15; if ( Vector3D.distance( fan.getPosition(), astroPosition ) < radius ){ state = “fan“; score -= 100; // Update the score display updateGUI(); // Display PopNeg100 MovieClip newPop(-1); // play sndFan sndFan.play(); } } for(i=mineA.length-1;i>=0;i--){ mine = mineA[i]; mine.rotateY(mine.userData.speed); if ( mine.visible && Vector3D.distance( mine.getPosition(), astroPosition ) < 10 ){ if ( state == “jump” ){ mine.visible = false; shakeFactor = 2; score += 100; updateGUI(); // Play the mine sound sndMine.play(); // Display Pop100 MovieClip newPop(1); } else if ( state == “run” ){ // It the astronaut was running, // astro die! :( 417
418 Chapter 10 n 3D Game Development astroP.visible = false; shakeFactor = 15; state = “die“; resetCounter = 120; // play sndDead sndDead.play(); // Display Crash MovieClip newCrash(); } } if (!mine.visible) { mineDown = mine.getDown(); astroDown = astroP.getDown(); if ( mineDown.dotProduct( astroDown ) < 0 ){ mine.visible = true; } } } if ( Vector3D.distance( energy.getPosition(), astroPosition ) < 10 ){ energyIndex = (energyIndex+1)%pointA.length; energy.copyTransformFrom( pointA[energyIndex] ); score += 1000; updateGUI(); energyCount++; // For every three energy boosts if ( energyCount == 3 ){ // Increment level level++; // Increase speed (used in astroF() ) speed += 0.2; // Increase the frame rate of astro. astro.frameSpeed += .1; // Add the levelGUI display to show user // they advanced a level. addChild(levelGUI); levelGUI.content.tf.text = level.toString(); levelGUI.play(); // reset energyCount energyCount = 0; // “Pause” user interaction with astro // while levelGUI is displayed resetCounter = 120; state = “energy“;
Version 04 } energyGUI.tf.text = energyCount.toString(); newPower(); sndReset.play(); } sky.rotateX(0.1); } private function cameraF():void{ Pivot3DUtils.setPositionWithReference(scene.camera, 0, 40, -30, astroP, 0.1); Pivot3DUtils.lookAtWithReference(scene.camera, 0, 0, 0, astroP, astroP.getUp(), 0.2 ); if ( shakeFactor > 0 ){ scene.camera.x += Math.random() * shakeFactor; scene.camera.y += Math.random() * shakeFactor; scene.camera.z += Math.random() * shakeFactor; shakeFactor *= 0.9; } } private function initGUI():void{ // Create and position user interface display. energyGUI = new EnergyGUI(); addChild(energyGUI); pointsGUI = new PointsGUI(); addChild(pointsGUI); pointsGUI.x = stage.stageWidth-pointsGUI.width; levelGUI = new LevelGUI(); levelGUI.x = stage.stageWidth/2; levelGUI.y = stage.stageHeight/2 } private function updateGUI():void { // update the points display pointsGUI.tf.text = score.toString(); } // Create and display MovieClip when points added (Pop100) // and subtracted (PopNeg100). private function newPop(n:int):void{ var pos:Vector3D = astroP.getScreenCoords(); if(n>0){ var pop:MovieClip = new Pop100(); } else { pop = new PopNeg100(); } pop.x = pos.x; 419
420 Chapter 10 n 3D Game Development pop.y = pos.y; addChild( pop ); } // When astro runs into a mine, display Crash MovieClip private function newCrash():void{ var pos:Vector3D = astroP.getScreenCoords(); var crash:Crash = new Crash(); crash.x = pos.x; crash.y = pos.y; addChild( crash ); } // Display Power MovieClip when energy boost gained. private function newPower():void{ var pos:Vector3D = astroP.getScreenCoords(); var power:Power = new Power(); power.x = pos.x; power.y = pos.y; addChild( power ); } private function cleanupF(e:Event):void{ scene.pause(); scene.removeEventListener( Scene3D.UPDATE_EVENT, updateF ); } private function shuffle(a:Vector.<Pivot3D>) { var i:int; var j:int; var e:*; var len:int = a.length; for (i = len-1; i>=0; i--) { j=Math.floor((i+1)*Math.random()); e = a[i]; a[i] = a[j]; a[j] = e; } } private function randomF(n1:Number,n2:Number):Number{ if(n1>n2){ return n2+(n1-n2)*Math.random(); } else { return n1+(n2-n1)*Math.random(); } } } }
Version 05 You can do much more with Flare3D, including adding physics (www.flare3d.com/ blog/2011/12/20/flare3d-physics-engine-beta) and 3D particle effects. Also, when you extract the flare3D.zip package, you will create an examples directory that contains a number of .as files with useful code snippets. In the final version of Yellow Planet, we’ll add particle effects using the ParticleEmiter3D class. We’ll also clean up some code; the commented code will show how you can change from a third-person view to a first-person view. Version 05 Version_05 GameView package com.kglad { import flare.basic.Scene3D; import flare.basic.Viewer3D; import flare.loaders.Flare3DLoader1 import flare.core.*; import flare.utils.*; import flare.system.Input3D; import flare.collisions.* import flash.display.*; import flash.events.*; import flash.geom.*; public class GameView extends Sprite { private var i:int; private var j:int; private var state:String = “run“; private var speed:Number; private var level:Number; private var jumpValue:Number; private var running:Boolean; private var score:int; private var bestScore:int; private var energyCount:int; private var energyIndex:int; private var shakeFactor:Number; private var resetCounter:int; private var radius:Number; private var angle:Number; private const halfPI:Number = Math.PI/2; private const threeHalvesPI:Number = 3*halfPI; // 3d objects private var scene:Scene3D; 421
422 Chapter 10 n 3D Game Development private var planet:Pivot3D; private var astro:Pivot3D; private var astroP:Pivot3D; private var shadow:Pivot3D; private var sky:Pivot3D; private var energy:Pivot3D; private var mine:Pivot3D; private var fan:Pivot3D; private var startPoint:Pivot3D // particles. private var smokeTexture:Texture3D; private var fireTexture:Texture3D; private var fireEmitter:FireEmitter; private var fanSmokeEmitter:SmokeEmitter ; private var mineSmokeEmitter:SmokeEmitter; // vectors private var mineA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var fanA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var pointA:Vector.<Pivot3D> = new Vector.<Pivot3D>(); private var smokeA:Vector.<SmokeEmitter> = new Vector.<SmokeEmitter>(); // collision variables private var astroPosition:Vector3D; private var astroCollision:RayCollision; private var sphereCollision:SphereCollision; private var astroAbove:Vector3D = new Vector3D(0,100,0); private var astroFrom:Vector3D = new Vector3D(); private var astroDown:Vector3D; private var mineDown:Vector3D; private var smokeDown:Vector3D; private var info:CollisionInfo; // GUI variables private var energyGUI:EnergyGUI; private var pointsGUI:PointsGUI; private var levelGUI:LevelGUI; // preloader private var loading:Loading = new Loading(); // sounds. private var sndDead:DeadSound = new DeadSound(); private var sndFan:FanSound = new FanSound(); private var sndJump:JumpSound = new JumpSound(); private var sndMine:MineSound = new MineSound(); private var sndReset:ResetSound = new ResetSound(); private var landSound:LandSound = new LandSound(); private var gameOverE:Event = new Event(“gameOverE“);
Version 05 public function GameView() { this.addEventListener(Event.ADDED_TO_STAGE,init,false,0,true); this.addEventListener(Event.REMOVED_FROM_STAGE,cleanupF,false,0,true); } private function init(e:Event):void{ if(!scene){ initGUI(); addChild( loading ); scene = new Scene3D(Sprite(root)); scene.showLogo = false; scene.pause(); scene.antialias = 1024; scene.camera = new Camera3D(); scene.addEventListener( Scene3D.PROGRESS_EVENT, progressF ); scene.addEventListener( Scene3D.COMPLETE_EVENT, completeF ); scene.registerClass( Flare3DLoader1); planet = scene.addChildFromFile( “models/planet.f3d” ); astro = scene.addChildFromFile( “models/astronaut.f3d” ); shadow = scene.addChildFromFile( “models/shadow.f3d” ); energy = scene.addChildFromFile( “models/energy.f3d” ); // Textures used for particle emitters smokeTexture = scene.addTextureFromFile( “assets/smoke.png” ); fireTexture = scene.addTextureFromFile( “assets/light.jpg”); } else { // I added a newGame() function to reset the // score, level, etc. newGame(); scene.resume(); scene.addEventListener( Scene3D.UPDATE_EVENT, updateF ); } } private function progressF(e:Event):void { loading.bar.gotoAndStop( int(scene.loadProgress) ); } private function completeF(e:Event):void { removeChild(loading); scene.removeEventListener( Scene3D.PROGRESS_EVENT, progressF ); scene.removeEventListener( Scene3D.COMPLETE_EVENT, completeF ); astroP = new Pivot3D(); astroP.addChild(astro); astroP.addChild(shadow); scene.addChild(astroP); 423
424 Chapter 10 n 3D Game Development // The FireEmitter and SmokeEmitter classes I copied and // tweaked using trial and error. This is another place // where the API is lacking. fireEmitter = new FireEmitter(fireTexture); // The fireEmitter display is below astro’s feet to // be displayed when he jumps. fireEmitter.parent = astro; fireEmitter.y = -4; sky = planet.getChildByName( “sky” ); astroCollision = new RayCollision(); astroCollision.addCollisionWith( planet.getChildByName( “floor” ), false ); sphereCollision = new SphereCollision( astroP, 3, new Vector3D( 0, 3, 0 ) ); planet.forEach(planetF); newGame(); scene.resume(); scene.addEventListener( Scene3D.UPDATE_EVENT, updateF ); } private function planetF(p3d:Pivot3D):void{ if(p3d.name.indexOf(“fan“)>-1){ p3d.userData = {speed:randomF(5,10)}; fanA.push(p3d); } else if(p3d.name.indexOf(“mine“)>-1){ p3d.userData = {speed:randomF(-20,-10),frequency:randomF(500,1000)}; mineA.push(p3d); } else if (p3d.name.indexOf(“obstacle“)>-1 ){ sphereCollision.addCollisionWith(p3d, false ); } else if (p3d.name.indexOf(“point“)>-1 ){ pointA.push(p3d); } } private function newGame():void{ speed = 0.75; jumpValue = 0; energyCount = 0; level = 0; Data.score = 0; score = 0; energyGUI.tf.text = energyCount.toString(); startGame(); }
Version 05 // newGame() is bypassed when astro collides with a fan. private function startGame():void { startPoint = planet.getChildByName( “start” ); astroP.copyTransformFrom( startPoint ); astro.gotoAndStop(6); astroP.visible = true; state = “run“; running = false; shakeFactor = 0; shuffle(pointA); energy.copyTransformFrom( pointA[energyIndex] ); } private function updateF(e:Event):void { astroF(); worldObjectsF(); cameraF(); updateGUI(); } private function astroF():void{ astroFrom= astroP.localToGlobal(astroAbove); astroDown = astroP.getDown(); if (astroCollision.test(astroFrom, astroDown)) { info = astroCollision.data[0]; astroP.setPosition( info.point.x, info.point.y, info.point.z ); astroP.setNormalOrientation( info.normal, 0.05 ); } sphereCollision.slider(); switch( state ){ case “run“: if ( Input3D.keyDown( Input3D.RIGHT ) ) { astroP.rotateY( 2 ); if(!running){ running = true; astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.RIGHT ) ) { running = false; astro.gotoAndStop(6); } if ( Input3D.keyDown( Input3D.LEFT ) ) { astroP.rotateY( -2 ); if(!running){ running = true; 425
426 Chapter 10 n 3D Game Development astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.LEFT ) ) { running = false; astro.gotoAndStop(6); } //astro.gotoAndStop( “run”,3); if ( Input3D.keyDown( Input3D.UP ) ) { astroP.translateZ( speed ); if(!running){ running = true; astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.UP ) ) { running = false; astro.gotoAndStop(6); } if ( Input3D.keyDown( Input3D.DOWN ) ) { astroP.translateZ( -speed ); if(!running){ running = true; astro.gotoAndPlay( “run”,3,0 ); } } else if ( Input3D.keyUp( Input3D.DOWN ) ) { running = false; astro.gotoAndStop(6); } if ( Input3D.keyHit( Input3D.SPACE ) ) { jumpValue = 4; // When astro jumps, fireEmitter // emits particles. fireEmitter.emitParticlesPerFrame = 25; state = “jump“; astro.gotoAndPlay( “jump”, 3 ); sndJump.play(); } break; case “jump“: if ( astro.y == 0 ){ state = “run“; if(!running){ astro.gotoAndStop(6); } }
Version 05 if ( Input3D.keyDown( Input3D.UP ) ) { astroP.translateZ( speed ); running = true; } if ( Input3D.keyDown( Input3D.DOWN ) ) { astroP.translateZ( -speed ); running = true } break; case “fan“: jumpValue = 4; astroP.rotateY(1); shakeFactor = 1; if ( astro.y > 500 ) { // fanSmokeEmitter created and // parented in worldObjectsF() // when astro is blown upwards // by a fan. fanSmokeEmitter.dispose(); startGame(); } break; case “energy“: resetCounter--; if ( resetCounter < 0 ) state = “run“; break; case “die“: resetCounter--; if ( resetCounter < 0 ) { dispatchEvent(gameOverE); } Data.score = score; break; } if(jumpValue!=0){ jumpValue -= 0.3; astro.y += jumpValue; if(state==“run” && !running){ shadow.scaleX = shadow.scaleZ = Math.max(.01, (500-astro.y)/500); } else { shadow.scaleX = shadow.scaleZ = Math.max(.01, (25-astro.y)/25); } 427
428 Chapter 10 n 3D Game Development if ( astro.y < 0 ) { if(state==“run” && !running){ landSound.play(); } jumpValue = 0; astro.y = 0; } } } private function worldObjectsF():void{ astroPosition = astro.getPosition(false); for(i=fanA.length-1;i>=0;i--){ fan = fanA[i]; fan.rotateY( fan.userData.speed ); radius = fan.scaleX * 15; if ( Vector3D.distance( fan.getPosition(), astroPosition ) < radius ){ state = “fan“; score -= 100; newPop(-1); sndFan.play(); // Here is where fanSmokeEmitter is created. // Only one can be created at any one time. fanSmokeEmitter = new SmokeEmitter( smokeTexture ); fanSmokeEmitter.copyTransformFrom( fan); fanSmokeEmitter.parent = scene; } } for(i=mineA.length-1;i>=0;i--){ mine = mineA[i]; mine.rotateY(mine.userData.speed); if ( mine.visible && Vector3D.distance( mine.getPosition(), astroPosition ) < 10 ){ if ( state == “jump” ){ mine.visible = false; shakeFactor = 2; score += 100; sndMine.play(); newPop(1); // When a mine is destroyed, // a new SmokeEmitter is // created and added.
Version 05 mineSmokeEmitter = new SmokeEmitter( smokeTexture ); mineSmokeEmitter.copyTransformFrom( mine); mineSmokeEmitter.scaleX = mineSmokeEmitter.scaleY = mineSmokeEmitter.scaleZ = .3; mineSmokeEmitter.emitParticlesPerFrame = 4; mineSmokeEmitter.parent = scene; // More than one can exist at any // one time, and I need to track // them all so they can be // disposed. smokeA.push(mineSmokeEmitter); } else if ( state == “run” ){ // If the astronaut was running, // astro die! :( astroP.visible = false; shakeFactor = 15; state = “die“; resetCounter = 120; sndDead.play(); newCrash(); } } if (!mine.visible) { mineDown = mine.getDown(); astroDown = astroP.getDown(); // This is another way to check if astro // is in the opposite hemisphere from a // destroyed mine. angle = Vector3D.angleBetween(mineDown,astroDown); if(angle>=halfPI && angle<=threeHalvesPI){ mine.visible = true; // If a mine is made visible, // there is a SmokeEmitter // instance that should be // disposed. for(j=smokeA.length-1;j>=0;j--){ smokeDown = smokeA[j].getDown(); angle = Vector3D.angleBetween(smokeDown,astroDown); 429
430 Chapter 10 n 3D Game Development if(angle>=halfPI && angle<=threeHalvesPI){ mineSmokeEmitter = smokeA[j]; smokeA.splice(j,1); mineSmokeEmitter.dispose(); } } } } } if ( Vector3D.distance( energy.getPosition(), astroPosition ) < 10 ){ energyIndex = (energyIndex+1)%pointA.length; energy.copyTransformFrom( pointA[energyIndex] ); score += 1000; energyCount++; if ( energyCount == 3 ){ level++; speed += 0.2; astro.frameSpeed += .1; addChild(levelGUI); levelGUI.content.tf.text = level.toString(); levelGUI.play(); energyGUI.tf.text = energyCount.toString(); energyCount = 0; resetCounter = 160; state = “energy”; } energyGUI.tf.text = energyCount.toString(); newPower(); sndReset.play(); } sky.rotateX(0.1); } private function cameraF():void{ // Third-person view Pivot3DUtils.setPositionWithReference(scene.camera, 0, 40, -30, astroP, 0.1); Pivot3DUtils.lookAtWithReference(scene.camera, 0, 0, 0, astroP, astroP.getUp(), 0.2 ); // You can change to a first-person view with the // following, but I think this game works better // with a third-person view. // First-person view
Version 05 //Pivot3DUtils.setPositionWithReference(scene.camera, // 0, 15, 0, astro, .9); //Pivot3DUtils.lookAtWithReference(scene.camera, // 0, 11, 10, astro, astroP.getUp(), .9); if ( shakeFactor > 0 ){ scene.camera.x += Math.random() * shakeFactor; scene.camera.y += Math.random() * shakeFactor; scene.camera.z += Math.random() * shakeFactor; shakeFactor *= 0.9; } } private function initGUI():void{ energyGUI = new EnergyGUI(); addChild(energyGUI); pointsGUI = new PointsGUI(); addChild(pointsGUI); pointsGUI.x = stage.stageWidth-pointsGUI.width; levelGUI = new LevelGUI(); levelGUI.x = stage.stageWidth/2; levelGUI.y = stage.stageHeight/2 } private function updateGUI():void { pointsGUI.tf.text = score.toString(); } private function newPop(n:int):void{ var pos:Vector3D = astroP.getScreenCoords(); if(n>0){ var pop:MovieClip = new Pop100(); } else { pop = new PopNeg100() } pop.x = pos.x; pop.y = pos.y; addChild( pop ); } private function newCrash():void{ var pos:Vector3D = astroP.getScreenCoords(); var crash:Crash = new Crash(); crash.x = pos.x; crash.y = pos.y; addChild( crash ); } 431
432 Chapter 10 n 3D Game Development private function newPower():void{ var pos:Vector3D = astroP.getScreenCoords(); var power:Power = new Power(); power.x = pos.x; power.y = pos.y; addChild( power ); } private function cleanupF(e:Event):void{ scene.pause(); scene.removeEventListener( Scene3D.UPDATE_EVENT, updateF ); } private function shuffle(a:Vector.<Pivot3D>) { var i:int; var j:int; var e:*; var len:int = a.length; for (i = len-1; i>=0; i--) { j=Math.floor((i+1)*Math.random()); e = a[i]; a[i] = a[j]; a[j] = e; } } private function randomF(n1:Number,n2:Number):Number{ if(n1>n2){ return n2+(n1-n2)*Math.random(); } else { return n1+(n2-n1)*Math.random(); } } } } Version_05 FireEmitter package com.kglad{ import flare.core.*; import flare.materials.*; import flare.materials.filters.*; public class FireEmitter extends ParticleEmiter3D { public function FireEmitter( texture:Texture3D ) { // Creates a ParticleMaterial3D to use with // ParticleEmiter3D. var material:Shader3D = new ParticleMaterial3D(); // Add filters
Version 05 material.filters.push( new TextureFilter( texture ) ); material.filters.push( new ColorParticleFilter( [ 0x334400], [ 1]) ); material.build(); // Call ParticleEmiter3D and pass the filtered material // and a FireParticle template. super( “fire”, material, new FireParticle() ); // Lets the particles use global coordinates instead // of being local to this. super.useGlobalSpace = true; // The number of particles to emit each frame. super.emitParticlesPerFrame = 2; // The duration of each particle (in frames). super.particlesLife = 50; // The emitParticlesPerFrame is decremented by this // amount per frame super.decrementPerFrame = 1; } } } import flare.core.*; import flash.geom.*; class FireParticle extends Particle3D{ private var spin:Number; private var velocity:Vector3D = new Vector3D(); override public function init( emitter:ParticleEmiter3D ):void { spin = 0.1; velocity.x = Math.random() * 0.2 - 0.1; velocity.z = Math.random() * 0.2 - 0.1; if ( emitter.useGlobalSpace ) velocity = emitter.localToGlobalVector( velocity ); this.x = Math.random() * 1 - 0.5; this.y = Math.random() * 1 - 0.5; this.z = Math.random() * 1 - 0.5; var scale:Number = 0.5; this.sizeX = scale; this.sizeY = scale; } override public function update(time:Number):void { this.x += velocity.x; this.z += velocity.z; this.sizeX *= 0.85; this.sizeY *= 0.85; } 433
434 Chapter 10 n 3D Game Development override public function clone():Particle3D { return new FireParticle(); } } Version_05 SmokeEmitter package com.kglad{ import flare.core.*; import flare.materials.*; import flare.materials.filters.*; public class SmokeEmitter extends ParticleEmiter3D { public function SmokeEmitter( texture:Texture3D ) { // Creates a ParticleMaterial3D instance to use // with ParticleEmiter3D. var material:Shader3D = new ParticleMaterial3D(); // Add filters material.filters.push( new TextureFilter( texture ) ); material.filters.push( new ColorParticleFilter( [ 0xffffff], [ 0.05 ] ) ); // Call ParticleEmiter3D and pass the filtered // material and a SmokeParticle template. super( “smoke”, material, new SmokeParticle() ); // The number of particles to emit each frame. super.emitParticlesPerFrame = 1; // The duration of each particle (in frames). super.particlesLife = 20; } } } import flare.core.*; class SmokeParticle extends Particle3D { private var speed:Number; private var spin:Number; override public function init( emitter:ParticleEmiter3D ):void { speed = Math.random() * 5; spin = .1; this.x = Math.random() * 20 - 10; this.y = Math.random() * 20 - 10; this.z = Math.random() * 20 - 10; var scale:Number = emitter.scaleX; this.sizeX = scale * 2; this.sizeY = scale * 2; }
Version 05 override public function update(time:Number):void{ this.y += speed; this.rotation += spin; this.sizeX *= 1.2; this.sizeY *= 1.2; } override public function clone():Particle3D { return new SmokeParticle(); } } 435
This page intentionally left blank
Chapter 11 Social Gaming: Social Networks Social gaming is game playing with some degree of social interaction with other people, including multiplayer games and games that have some relationship to a social network. In this chapter, I’ll address some of the ways you can use the social networks Facebook, Twitter, and Google+ in your game. Depending on the amount of functionality you want and the amount of work you are willing to do, there are different ways to integrate your game with each of these social networking sites. But no matter how you choose to integrate, you will need to upload your game to a server. Also, Facebook, Twitter, and Google+ have to contend with security issues that lead to a multistep authentication process involving your game, the user, and the social network. When you retrieve data from a social network, there are three participants to consider: the user who is playing your game, your game, and your code that is trying to access the data and the social network/data provider (Facebook, Twitter, or Google+). To access non-public information from Facebook, Twitter, and Google+, those data suppliers will need the user’s login and password, as well as user permission before your game can be authorized to access or make changes to data on the user’s behalf. Obviously, the user shouldn’t reveal his password to you or your game, so each of those sites has set up a multistep authentication model (implementing the OAuth 2.0 protocol) whereby your game is given a token (with permissions granted by the user) that allows your game (limited) ability to retrieve, edit, add, and delete data on the user’s behalf. 437
438 Chapter 11 n Social Gaming: Social Networks To furnish your game with a token, Facebook, Twitter, and Google+ have to know some things about your game (requiring game registration), they have to authenticate the user (requiring the user to log in), and they need some way to determine whether the user wants to allow your game to retrieve, add, edit, or delete (some) data on their behalf. At its most basic, you register your game with the data provider. Your game is then assigned a unique identifier that identifies the game to the data provider. When you want to access data on behalf of a user, your identifier is transmitted to the data provider. Facebook The simplest way to hook into Facebook is to use their plug-ins to add a Like button, Send button, Login button, and so on to your game. If you only want to add a Like button to a page on your website, you can use the following iframe tag anywhere (between the body tags) in your HTML document where you want the Facebook Like button to appear: <iframe src="http://www.facebook.com/plugins/like.php?href=yoursite.com/subdirectory/ page.html" scrolling="no" frameborder="0" style="border:none; width:500px; height:25px"> </iframe> where the example: href attribute is equal to the absolute URL to your HTML file. For <iframe src="http://www.facebook.com/plugins/like.php?href=kglad.com/Files/fb/" scrolling="no" frameborder="0" style="border:none; width:500px; height:25px"> </iframe> points to index.html in kglad/com/Files/fb. However, to get the most out of Facebook, you will need to use JavaScript or the Facebook ActionScript API and register your game on the Facebook Developer App page. Before you can register your game with Facebook, you need to be registered with Facebook, and you need to log in. Once you’re logged in, you can go to https://developers.facebook.com/apps and click Create New App to register your game with Facebook (see Figure 11.1).
Facebook Figure 11.1 Facebook’s Create New App form. Source: Facebook® Inc. Enter your app’s name and click Continue. Complete the security check (see Figure 11.2) and click Submit. You should see the App Dashboard, where you enter details about your game (see Figure 11.3). Figure 11.2 Security Check form. Source: Facebook® Inc. 439
440 Chapter 11 n Social Gaming: Social Networks Figure 11.3 App Dashboard with Basic settings selected. Source: Facebook® Inc. If you mouse over a question mark, you will see an explanation of what is required to complete this form. Enter a namespace (a string that allows you to group and keep your games separate from like-named games), from the Category selection pick Games, and then select a subcategory. Your game can integrate with Facebook in several ways, listed above the Save Changes button. You can select more than one, but for now select only Website with Facebook Login and App on Facebook, enter the fully qualified URL to your game, and click Save Changes (see Figure 11.4).
Facebook Figure 11.4 Facebook integration menu expanded. Source: Facebook® Inc. You can return to https://developers.facebook.com/apps any time and edit your entries. Your games will be listed on the left, and you can use the Edit App button to make changes and the Create New App button to add another game. Click on the App Center link on the left (see Figures 11.5a and 11.5b). Fill in everything that isn’t optional and upload all the required images. Again, if you mouse over the question marks, you will see requirement details, including exact image sizes for the icons, banners, and screenshots. 441
442 Chapter 11 n Social Gaming: Social Networks Figure 11.5a Top half of the App Center form. Source: Facebook® Inc.
Facebook Figure 11.5b Bottom half of the App Center form. Source: Facebook® Inc. When you’re finished, click Save Changes and Submit App Detail Page. You should then see Additional Notes and Terms (see Figure 11.6). Figure 11.6 The Additional Notes and Terms page. Source: Facebook® Inc. 443
444 Chapter 11 n Social Gaming: Social Networks Check the check boxes and click Review Submission. If everything looks acceptable, click Submit. You should then see something like what’s shown in Figure 11.7. Figure 11.7 After you agree to Facebook’s terms, you should be directed back to the App Center, where you will see this modal window. Source: Facebook® Inc. You are now ready to integrate your game with Facebook’s API. Because there is a Facebook ActionScript API that communicates with Facebook’s API, you need not work directly with the Facebook API. But before I cover Adobe’s Facebook ActionScript API, I’m going to cover how you can use the Facebook JavaScript API directly. There’s no reason to actually do that while the Facebook ActionScript API still works, but by the time you read this, the Facebook ActionScript API may no longer work. In addition, this section shows the basics needed to use any JavaScript API, so it applies to any social network that has a JavaScript API, including the next “hot” social network that will arise in the future. Facebook JavaScript API In this section I’ll show you how you would use the Facebook JavaScript API so you can see how to use a JavaScript API with the next hot social networking site before
Facebook an ActionScript API is available or with Facebook if they change their cross-domain policy file (like Twitter did). Facebook had a REST API (http://en.wikipedia.org/wiki/Representational_state_ transfer) that they deprecated in favor of what they are calling their Graph API. Their Graph API looks more like a name change than a change from the RESTful principles that you can read about at the previously referenced Wikipedia page, if you’re interested. In any case, without diverging into theory, you can use Facebook with http requests (http://developers.facebook.com/docs/reference/api). I’ll cover that in the “Google+” section. If there were no ActionScript API for Facebook, you could use the Facebook JavaScript API to communicate between your game and Facebook. That JavaScript should be in your SWF’s embedding HTML page. (There is a way to inject JavaScript into the embedding HTML page using ActionScript that I will cover in the “Google+” section.) © 2013 Keith Gladstien, All Rights Reserved. And you would need to use the ActionScript ExternalInterface class to communicate between the JavaScript in your embedding HTML page and the ActionScript in your SWF (see Figure 11.8). Figure 11.8 Schematic diagram showing how ActionScript can communicate with Facebook. Everything on the left is in your SWF’s embedding HTML page. To simplify the Facebook and ActionScript communication, I will start by explaining ActionScript and JavaScript communication (see Figure 11.9). 445
Chapter 11 n Social Gaming: Social Networks © 2013 Keith Gladstien, All Rights Reserved. 446 Figure 11.9 Schematic diagram showing how ActionScript can communicate with JavaScript. ActionScript and JavaScript Communication We need ActionScript code that sends data to JavaScript and ActionScript code that listens for data that is sent from JavaScript to ActionScript. Here is the ActionScript code used to send the variable some_data’s value from ActionScript to JavaScript: ExternalInterface.call("fromSWF", some_data); Here is the ActionScript code used to listen for data that is sent from JavaScript to ActionScript: ExternalInterface.addCallback("swfF",this.fromJS); function fromJS(value:String):void { // do something with value } To use either of those, you need to import the ExternalInterface class: import flash.external.ExternalInterface; Both the call and addCallback methods are static methods of the ExternalInterface class and therefore are always applied exactly as shown. The first parameter in the call method is the JavaScript function name (always in quotes), and the second parameter is the (optional) data you want to send to the JavaScript function. In the context of this chapter, that second parameter should be limited to a String, an Array, or an associative Array. The first parameter of the addCallback method is the JavaScript function name (in quotes), and the second parameter is an in-scope (of the addCallback code) function name (not in quotes).
Facebook We also need JavaScript functions fromSWF and swfF. They can be something as simple as: <script language="JavaScript"> function thisMovie(movieName) { if (navigator.appName.indexOf("Microsoft") != -1) { return window[movieName]; } else { return document[movieName]; } } function toSWF(value) { thisMovie("your_swf_name_without_the_file_extension").swfF(value); } function fromSWF(value) { // You can return a value from this function, and it // will be returned to // ExternalInterface.call("fromSWF",some_data). return "test return from js fromSWF()"; } </script> The thisMovie function should not be changed. It is needed for JavaScriptto-ActionScript communication. The toSWF function calls thisMovie and passes the name of your SWF file (without the .swf suffix and in quotes). If you change thisMovie or fail to pass the correct SWF name to thisMovie, JavaScript to ActionScript communication will fail. Appended to thisMovie(swf_name) is the function name (without quotes) used in the ActionScript addCallback method (with quotes). In addition to all that, you need to use SWF embedding code that is compatible with the ExternalInterface class. Here is sample embedding code that works with ExternalInterface and embeds game_01.swf. <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="game_01" width="500" height="375"> <param name="movie" value="game_01.swf" /> <param name="quality" value="high" /> <param name="bgcolor" value="#ffffff" /> <param name="allowScriptAccess" value="sameDomain" /> <embed src="game_01.swf" quality="high" bgcolor="#ffffff" width="500" height="375" name="game_01" align="middle" play="true" loop="false" quality="high" allowScriptAccess="sameDomain" type="application/x-shockwave-flash" 447
448 Chapter 11 n Social Gaming: Social Networks pluginspage="http://www.macromedia.com/go/getflashplayer"> </embed> </object> Notice the allowScriptAccess parameter. That parameter specifies whether the embedded SWF and the embedding HTML page can communicate. If the value is “never”, no communication is allowed. If it is “sameDomain”, communication is allowed only when the SWF and HTML are in the same domain. And, if the value is “always”, the SWF and HTML file can always communicate, even if they are in different domains. This isn’t the only code you can use to embed a SWF and work with ExternalInterface. You can also use SWFObject (http://code.google.com/p/swfobject) to embed a SWF, and it works well with ExternalInterface. With both embedding methods, you only need to change the SWF name references, stage size, and background color in the embedding code. With the above embedding code, there are four references to game_01 that you should change to match the name of your SWF, and you should change the width, height, and bgcolor in the two locations to match your SWF’s width, height, and background color. With the above embedding code for game_01, the function toSWF(value) { thisMovie("game_01").swfF(value); } toSWF function should be: // change game_01 to match your swf’s name Putting all that together gives the following embedding HTML file code and the following ActionScript document class code. These files are in /support files/Chapter 11/ facebook/fb1. Version _0a Version _0a index.html <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html"; charset=utf-8 /> <title>game_01</title> <script language="JavaScript"> function thisMovie(movieName) { if (navigator.appName.indexOf("Microsoft") != -1) { return window[movieName]; } else { return document[movieName]; } }
Facebook function toSWF(value) { thisMovie("game_01").swfF(value); } function fromSWF(value) { // If you have firebug installed in firefox, you can use // console.log to display data. console.log("ActionScript conveys: "+value+"\n"); // Otherwise, you can use the alert function. // alert("ActionScript sends: "+value); } </script> </head> <body> <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="game_01" width="500" height="375"> <param name="movie" value="game_01.swf" /> <param name="quality" value="high" /> <param name="bgcolor" value="#ffffff" /> <param name="allowScriptAccess" value="sameDomain" /> <embed src="game_01.swf" quality="high" bgcolor="#ffffff" width="500" height="375" name="game_01" align="middle" play="true" loop="false" quality="high" allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"> </embed> </object> </body> </html> Version _0a Main package { import flash.display.MovieClip; import flash.display.Sprite; import flash.text.TextField; import flash.events.MouseEvent; // This is needed to use ExternalInterface import flash.external.ExternalInterface; public class Main extends MovieClip { private var input_tf:TextField; private var output_tf:TextField; private var sendBtn:Sprite; public function Main() { // Create an input textfield that can be used to // test if data from the textfield can be passed // to JavaScript. 449
450 Chapter 11 n Social Gaming: Social Networks inputF(); // Create a send button to trigger that data send. sendF(); // Create an output textfield that can be used to // test if data from JavaScript is successfully // passed to ActionScript. outputF(); } private function inputF():void { input_tf = new TextField(); input_tf.type="input"; input_tf.background=true; input_tf.border=true; input_tf.width=360; input_tf.height=18; addChild(input_tf); } private function sendF():void { sendBtn = new Sprite(); sendBtn.mouseEnabled=true; sendBtn.x=input_tf.width+10; sendBtn.graphics.beginFill(0xb0b0b0); sendBtn.graphics.drawRect(0,0,80,20); sendBtn.graphics.endFill(); var tf:TextField = new TextField(); tf.multiline=false; tf.text="SEND"; tf.autoSize="left"; tf.x = (sendBtn.width-tf.width)/2; sendBtn.addChild(tf); sendBtn.mouseChildren=false; sendBtn.buttonMode=true; sendBtn.addEventListener(MouseEvent.CLICK, dataSendF); addChild(sendBtn); } private function outputF():void { output_tf = new TextField(); output_tf.y=25; output_tf.width=450; output_tf.height=325; output_tf.multiline=true; output_tf.wordWrap=true; output_tf.border=true; output_tf.text="";
Facebook addChild(output_tf); // Here is the only line of ActionScript code that // registers a function (this.fromJS) to receive data // from the JavaScript swfF ExternalInterface.addCallback("swfF",this.fromJS); } private function dataSendF(e:MouseEvent):void { // Here is the only line of ActionScript code that // calls the JavaScript function fromSWF (and the // text from input_tf is passed. ExternalInterface.call("fromSWF",input_tf.text); //ExternalInterface.call("notesF"); } private function fromJS(value:String):void { output_tf.appendText("JavaScript callback to ActionScript: " + value + "\n"); } } } Rename index_0a.html to index.html and Main_0a.as to Main.as. Then open game_01.fla in Flash Pro and publish game_01.swf. You don’t need to publish an HTML file from Flash, and if you do, you don’t want to use it or overwrite the HTML file with the above code. If you have HTML ticked in the publish settings, Flash will overwrite any same-named HTML file without warning that you are about to overwrite a file, so check your publish settings before publishing. You can then open index.html in your web browser and test communication from ActionScript to JavaScript and from JavaScript to ActionScript. The HTML document contains close to the minimum amount of code you need to communicate between JavaScript and ActionScript. The ActionScript in Main contains much more than the minimum amount of code because it creates input and output textfields and a send button, all used to facilitate testing of the JavaScript and ActionScript communication. The actual ActionScript code needed to implement ActionScript and JavaScript communication is contained in about six lines of code. JavaScript and Facebook Communication The minimum amount of code to add the Facebook JavaScript API to your embedding HTML is the following, placed just below your opening body tag. In the two places appId appears below, you will need to change it from the App ID for game_01 (refer to Figure 11.3) to the App ID Facebook assigned to your game when you registered it with Facebook. 451
452 Chapter 11 n Social Gaming: Social Networks <div id="fb-root"></div> <script> // Load the Facebook JavaScript API (function(d, s, id) { var js, fjs = d.getElementsByTagName(s)[0]; if (d.getElementById(id)) return; js = d.createElement(s); js.id = id; js.src = "//connect.facebook.net/en_US/all.js#xfbml=1&appId=229095550543776"; fjs.parentNode.insertBefore(js, fjs); }(document, ’script’, ‘facebook-jssdk’)); // initialize Facebook JavaScript (after it loads) with your App ID window.fbAsyncInit = function() { FB.init({ appId : ‘229095550543776’, // App ID status : true, // check login status cookie : true, // enable cookies to allow the server to access the session xfbml : true // parse XFBML }); }; </script> According to the Facebook documents, the fb-root div is needed. I don’t see any reason to remove it, but I also saw no problem removing it. I’m not sure what it does. There was legacy JavaScript API loading code that appended elements to that div, but the most recent loading code uses the script tag. Because it may be needed for some parts of the API and for some browsers, it is probably best left as is. You can’t do anything useful with that code alone. But starting with that code, you can access Facebook’s JavaScript API. Actually, Facebook calls it their JavaScript Software Development Kit (SDK), which includes their Graph API, and their Graph API is the main JavaScript API for webbased applications. But the SDK also includes the Facebook Dialogs, such as the Feed Dialog, Pay Dialog, and so on. Warning: The Facebook developer documents appear to be written by different people at different times, so there is a noticeable lack of consistency, clarity, and organization. Nevertheless, there is a lot of useful information there. Here is the link to their JavaScript API: http://developers.facebook.com/docs/reference/JavaScript.
Facebook And here is the link to the Facebook permissions page, which you’ll need to use if you want to access anything other than public information about a user: https:// developers.facebook.com/docs/authentication/permissions. I will explain how permissions work later. For now, open the JavaScript API reference page. There are three main methods of the JavaScript API. 1. FB.init, which you must use to connect to the API and which I used in the previous JavaScript code. 2. FB.api, which you use to connect to Facebook’s Graph API, which taps into all the connections that Facebook users have to everything, including other users, events, pictures, and so on. 3. FB.ui, which you use to connect to the Facebook Dialogs. Notice these are all static methods. In fact, the entire API uses static methods. If you click the FB.ui link, you will see six more links, some of which show sample JavaScript code for using the FB.ui method. I don’t understand the inconsistency, but the code that is shown works. In a moment, I will show a list of FB.ui methods you can use. If you click the FB.api link, you will see another link to the Graph API and an explanation that FB.api is used to call the Graph API. There is also sample JavaScript code that shows how to use the FB.api. This is good. If you click the Graph API link, you are taken to a page that has no JavaScript and, in fact, doesn’t appear to be related to the JavaScript API. Nevertheless, you can use that page to determine the FB.api calls you can make. I will present a list of FB.api calls after showing you how to use FB.api calls. But it’s easier to use the FB.ui method, so that’s a good starting point. The next section covers the FB.ui or Facebook Dialog methods. Facebook Dialogs Because the Dialogs require explicit user interaction (and so explicit approval by the user), there is no need to seek permissions and no need to check whether the user is logged into Facebook. If the user is not logged in, the log-in Dialog will be presented, followed by the specific Dialog that you called. If the user is logged in, only the called Dialog will pop up. 453
454 Chapter 11 n Social Gaming: Social Networks Note that you should use a mouse click to call the Dialog; otherwise, the user’s popup blocker might block the dialog pop-up. Here is the sample code to call an FB.ui method or Facebook Dialog. Version _0b Version _0b index.html <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>game_01</title> <script language="JavaScript"> function thisMovie(movieName) { if (navigator.appName.indexOf(“Microsoft“) != -1) { return window[movieName]; } else { return document[movieName]; } } function toSWF(value) { thisMovie(“game_01“).swfF(value); } function fromSWF(value) { console.log(“ActionScript conveys: “+value+“\n“); // alert(“ActionScript sends: “+value); } // sendF is called by ActionScript ExternalInterface.call(“sendF“) function sendF(){ // The object obj is defined below. It has 3 properties: method, // name and link. // The method property of FB.ui objects specifies which dialog is // presented to the user. Using “send” triggers a dialog that // allows users to share a link. // The recipient receives the notice in their Message inbox. var obj = { method: ’send’, name: ‘Check game_01’, link: ‘http://www.kglad.com/Files/fb’, } // The first parameter in FB.ui accepts an object, and the second // parameter is a callback function that Facebook uses to return // (sometimes) useful data (response) after the FB.ui call. FB.ui(obj,function(response){ toSWF(“send response**“); // To determine what is returned, I created an evalF
Facebook // function that iterates through the returned parameter. evalF(response); }); } // If obj is an object, this function iterates through the // object properties. Because Facebook can and does nest // objects, this function is recursive. function evalF(obj){ if(typeof obj == “object“){ for(var s in obj){ toSWF(s); evalF(obj[s]); } } else { toSWF(obj); } } </script> </head> <body> <div id="fb-root"></div> <script> // load fb js sdk (function(d, s, id) { var js, fjs = d.getElementsByTagName(s)[0]; if (d.getElementById(id)) return; js = d.createElement(s); js.id = id; js.src = "//connect.facebook.net/en_US/all.js#xfbml=1&appId=229095550543776"; fjs.parentNode.insertBefore(js, fjs); }(document, ’script’, ‘facebook-jssdk’)); // initialize with fb js (after it loads) with your App ID window.fbAsyncInit = function() { FB.init({ appId : ‘229095550543776’, // App ID status : true, // check login status cookie : true, // enable cookies to allow the server to access the session xfbml : true // parse XFBML }); }; </script> <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="game_01" width="500" height="375"> <param name="movie" value="game_01.swf" /> <param name="quality" value="high" /> 455
456 Chapter 11 n Social Gaming: Social Networks <param name="bgcolor" value="#ffffff" /> <param name="allowScriptAccess" value="sameDomain" /> <embed src="game_01.swf" quality="high" bgcolor="#ffffff" width="500" height="375" name="game_01" align="middle" play="true" loop="false" quality="high" allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"> </embed> </object> </body> </html> Version _0b Main.as package com.kglad{ import flash.display.MovieClip; import flash.display.Sprite; import flash.text.TextField; import flash.events.MouseEvent; // This is needed to use ExternalInterface import flash.external.ExternalInterface; public class Main extends MovieClip { private var input_tf:TextField; private var output_tf:TextField; private var sendBtn:Sprite; public function Main() { // Create an input textfield that can be used // to test if data from the textfield can be // passed to JavaScript. inputF(); // Create a send button to trigger that data send. sendF(); // Create an output textfield that can be used // to test if data from JavaScript is // successfully passed to ActionScript. outputF(); } private function inputF():void { input_tf = new TextField(); input_tf.type="input"; input_tf.background=true; input_tf.border=true; input_tf.width=360; input_tf.height=18; addChild(input_tf);
Facebook } private function sendF():void { sendBtn = new Sprite(); sendBtn.mouseEnabled=true; sendBtn.x=input_tf.width+10; sendBtn.graphics.beginFill(0xb0b0b0); sendBtn.graphics.drawRect(0,0,80,20); sendBtn.graphics.endFill(); var tf:TextField = new TextField(); tf.multiline=false; tf.text="Send a Message"; tf.autoSize="left"; tf.x = (sendBtn.width-tf.width)/2; sendBtn.addChild(tf); sendBtn.mouseChildren=false; sendBtn.buttonMode=true; sendBtn.addEventListener(MouseEvent.CLICK, dataSendF); addChild(sendBtn); } private function outputF():void { output_tf = new TextField(); output_tf.y=25; output_tf.width=450; output_tf.height=325; output_tf.multiline=true; output_tf.wordWrap=true; output_tf.border=true; output_tf.text=""; addChild(output_tf); ExternalInterface.addCallback("swfF",this.fromJS); } private function dataSendF(e:MouseEvent):void { // Call the JavaScript function sendF, which // calls an FB.ui method that triggers a Facebook // Send Message pop-up. ExternalInterface.call("sendF"); } private function fromJS(value:String):void { output_tf.appendText("JavaScript callback to ActionScript: " + value + "\n"); } } } 457
458 Chapter 11 n Social Gaming: Social Networks The only new things in index_0b.html are sendF and evalF. evalF is just a helper function I added to see what Facebook is returning. sendF contains the FB.ui code that calls the Send Message Dialog. To use the other Facebook Dialogs, you need to make only minor changes to the embedding HTML JavaScript and Flash ActionScript. For example, to prompt a user to post a feed to his or her wall, you can add between the head script tags (see index_0c.html): feedF function feedF(){ var obj = { method: ‘feed’, link: ‘http://www.kglad.com/Files/fb’, picture: ‘http://www.kglad.com/Files/fb/game_01.jpg’, name: ‘game_01 feed test’, caption: ‘game_01’, description: ‘game_01 feed dialog.’ }; FB.ui(obj, function(response){ toSWF("feed response **"); evalF(response); }); } And change ExternalInterface.call(“sendF”) to com.kglad.Main_0c on the book’s website.) ExternalInterface(“feedF”). (See Here is that promised list of FB.ui methods, followed by the last version of index and Main dealing with the FB.ui methods. // Prompt user to add a friend function addFriendF(){ var obj = { method: ‘friends.add’, id: "100004023276213" } FB.ui(obj, function(response){ toSWF("add friend"); evalF(response); }); } // If your game can accept in-game payments (check the App Dashboard). // Payments documents: http://developers.facebook.com/docs/credits/build/ // Prompt user to buy something
Facebook function buyF() { var obj = { method: ‘pay’, action: ‘buy_item’, // You can pass any string, but your payments_get_items must // be able to process and respond to this data. order_info: {’item_id’: ‘1a’}, dev_purchase_params: {’oscif’: true} }; FB.ui(obj, function(response){ toSWF("buy"); evalF(response); }); } // Post to feed function feedF(){ var obj = { method: ‘feed’, link: ‘http://www.kglad.com/Files/fb’, picture: ‘http://www.kglad.com/Files/fb/game_01.jpg’, name: ‘game_01 feed test’, caption: ‘game_01’, description: ‘game_01 feed dialog.’ }; FB.ui(obj, function(response){ toSWF("feed response **"); evalF(response); }); } // If something fails silently, you need to request permission // Request permissions for non-public data function permissionsF(){ var obj = { method: ‘permissions.request’, perms: ‘user_birthday,user_relationship_details,read_stream’, display: ‘popup’ }; FB.ui(obj, function(response) { toSWF("permissions **"); evalF(response); }); } // Send a request from the current user to one or more recipients function requestF(){ 459
460 Chapter 11 n Social Gaming: Social Networks var obj = { method: ‘apprequests’, message: ‘Join Me!’ } FB.ui(obj, function(response){ toSWF("request reponse **"); evalF(response); }); } // Share a link function sendF(){ var obj = { method: ’send’, name: ‘Check game_01’, link: ‘http://www.kglad.com/Files/fb’, } FB.ui(obj,function(response){ toSWF("send response**"); evalF(response); }); } Version _0d version _0d index.html <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>game_01</title> <script language="JavaScript"> function thisMovie(movieName) { if (navigator.appName.indexOf("Microsoft") != -1) { return window[movieName]; } else { return document[movieName]; } } function toSWF(value) { thisMovie("game_01").swfF(value); } function fromSWF(value) { // If you have firebug installed in firefox, you can use // console.log to display data. console.log("ActionScript conveys: "+value+"\n");
Facebook // Otherwise, you can use the alert function. // alert("ActionScript sends: "+value); } // Prompt user to add a friend function addFriendF(){ var obj = { method: ‘friends.add’, id: 123456789 } FB.ui(obj, function(response){ toSWF("add friend"); evalF(response); }); } // If your game can accept in-game payments (check the App Dashboard): // http://developers.facebook.com/docs/credits/build/ function buyF() { var obj = { method: ‘pay’, action: ‘buy_item’, // You can pass any string, but your payments_get_items must // be able to process and respond to this data. order_info: {’item_id’: ‘1a’}, dev_purchase_params: {’oscif’: true} }; FB.ui(obj, function(response){ toSWF("buy"); evalF(response); }); } // Post to feed function feedF(){ var obj = { method: ‘feed’, link: ‘http://www.kglad.com/Files/fb’, picture: ‘http://www.kglad.com/Files/fb/game_01.jpg’, name: ‘game_01 feed test’, caption: ‘game_01’, description: ‘game_01 feed dialog.’ }; FB.ui(obj, function(response){ toSWF("feed response **"); evalF(response); 461
462 Chapter 11 n Social Gaming: Social Networks }); } // Request permissions for non-public data // https://developers.facebook.com/docs/authentication/permissions/ function permissionsF(){ var obj = { method: ‘permissions.request’, perms: ‘user_birthday,user_relationship_details,read_stream’, display: ‘popup’ }; FB.ui(obj, function(response) { toSWF("permissions **"); evalF(response); }); } // Send a request from the current user to one or more recipients function requestF(){ var obj = { method: ‘apprequests’, message: ‘Join Me!’ } FB.ui(obj, function(response){ toSWF("request reponse **"); evalF(response); }); } // sendF is called by ActionScript ExternalInterface.call("sendF") function sendF(){ // The object obj is defined below. It has 3 properties: // method, name, and link. // The method property of FB.ui objects specifies which dialog // is presented to the user. Using "send" triggers a dialog that // allows users to share a link. // The recipient receives the notice in their Message inbox. var obj = { method: "send", name: ‘Check game_01’, link: ‘http://www.kglad.com/Files/fb’, } // The first parameter in FB.ui accepts an object, and the second // parameter is a callback function that Facebook uses to return // (sometimes) useful data (response) after the FB.ui call. FB.ui(obj,function(response){ toSWF("send response**");
Facebook // To determine what is returned, I created an evalF // function that iterates through the returned parameter. evalF(response); }); } // If obj is an object, this function iterates through the // object properties. // Because Facebook can and does nest objects, this function // is recursive. function evalF(obj){ if(typeof obj == "object"){ for(var s in obj){ toSWF(s); evalF(obj[s]); } } else { toSWF(obj); } } </script> </head> <body> <div id="fb-root"></div> <script> // load fb js sdk (function(d, s, id) { var js, fjs = d.getElementsByTagName(s)[0]; if (d.getElementById(id)) return; js = d.createElement(s); js.id = id; js.src = "//connect.facebook.net/en_US/all.js#xfbml=1&appId=229095550543776"; fjs.parentNode.insertBefore(js, fjs); }(document, ’script’, ‘facebook-jssdk’)); // initialize with fb js (after it loads) with your App ID window.fbAsyncInit = function() { FB.init({ appId : ‘229095550543776’, // App ID status : true, // check login status cookie : true, // enable cookies to allow the server to access the session xfbml : true // parse XFBML }); }; </script> 463
464 Chapter 11 n Social Gaming: Social Networks <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="game_01" width="500" height="375"> <param name="movie" value="game_01.swf" /> <param name="quality" value="high" /> <param name="bgcolor" value="#ffffff" /> <param name="allowScriptAccess" value="sameDomain" /> <embed src="game_01.swf" quality="high" bgcolor="#ffffff" width="500" height="375" name="game_01" align="middle" play="true" loop="false" quality="high" allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"> </embed> </object> </body> </html> Version _0d Main.as package com.kglad{ import flash.display.MovieClip; import flash.display.Sprite; import flash.text.TextField; import flash.events.MouseEvent; // This is needed to use ExternalInterface import flash.external.ExternalInterface; public class Main extends MovieClip { private var output_tf:TextField; private var btn:Sprite; private var tf:TextField; private var returnS:String; private var btnA:Array = ["Add Friend","Buy","Feed","Permissions","Request","Send"]; private var nextX:int = 0; private var gapX:int = 5; public function Main() { for(var i:int=0;i<btnA.length;i++){ buttonF(i); } outputF(); } private function buttonF(i:int):void { tf = new TextField(); with(tf){ multiline=false; text=btnA[i];
Facebook autoSize="left"; } btn = new Sprite(); with(btn){ name = i.toString(); mouseEnabled=true; mouseChildren = false; buttonMode = true; x=nextX; graphics.beginFill(0xb0b0b0); graphics.drawRect(0,0,tf.width+10,20); graphics.endFill(); } nextX += btn.width+gapX; tf.x = (btn.width-tf.width)/2; btn.addChild(tf); btn.addEventListener(MouseEvent.CLICK, dataSendF); addChild(btn); } private function outputF():void { output_tf = new TextField(); output_tf.y=25; output_tf.width=450; output_tf.height=325; output_tf.multiline=true; output_tf.wordWrap=true; output_tf.border=true; output_tf.text=""; addChild(output_tf); ExternalInterface.addCallback("swfF",this.fromJS); } private function dataSendF(e:MouseEvent):void { // stringF() just converts the strings in btnA // to match the corresponding JavaScript function // (as a string). ExternalInterface.call(stringF(btnA[int(e.currentTarget. name)])); } private function stringF(s:String):String{ returnS = s.split(" ").join("")+"F"; return returnS.substr(0,1).toLowerCase()+returnS.substr(1); } private function fromJS(value:String):void { 465
466 Chapter 11 n Social Gaming: Social Networks output_tf.appendText("JavaScript callback to ActionScript: " + value + "\n"); } } } I hard-coded several things that will not be typically hard-coded in your game. For example, permissionsF will almost certainly not have the perms property hard-coded. You will pass a permissionS string to permissionsF specifying the permissions you need and using the strings listed on https://developers.facebook.com/docs/authentication/ permissions. To access non-public data, you will need to explicitly obtain the user’s permission for each and every non-public datum you want to access. Because most users will (or at least should) be reluctant to grant those permissions without some explanation and understanding of what you will do with that data, you should minimize your requests, explain them, and only make a request when it will benefit the user. Otherwise, you’ll get many denied requests. So, it is better to call permissionsF several times, requesting one permission each time so you can quickly explain to the user the benefit of granting that specific permission. // Request permissions for non-public data function permissionsF(permissionS){ var obj = { method: ‘permissions.request’, perms: permissionS, display: ‘popup’ }; FB.ui(obj, function(response) { toSWF("permissions **"); evalF(response); }); } Facebook Social Graph You will use the FB.api methods to access the Facebook social graph, which consists of Facebook users and all their connections to other users (their friends), events, music, photos, and so on. If you have obtained the appropriate permissions, the data returned from Facebook to your game isn’t noticeable by the user. That is, there are no pop-ups if the user is logged in and no new permissions are needed, and it’s not necessary to have the user click a button to trigger queries to Facebook.
Facebook When a user links to your game, you have to check whether the user is logged in. If he is not and your game uses Facebook data, you need to offer a log-in button. I’ll show you how to do that below, after I show you how to check whether the user is logged in. You should do that check when the Facebook JavaScript API is initialized, which occurs when window.fbAsyncInit executes. Here is sample code: window.fbAsyncInit = function() { FB.init({ appId : ‘229095550543776’, // App ID status : true, // check login status cookie : true, // enable cookies to allow the server to access the session xfbml : true // parse XFBML }); // fbLoginStatus is called when there is a login status change // and when fb js initializes function fbLoginStatus(response) { // If the user is logged in, response.status is ‘connected’ if (response.status === ‘connected’) { FB.api(’/me’, function(response) { toSWF("is connected:: "+response.status); evalF(response); }); } else { // The user is not logged in. toSWF("not connected:: "+response.status); evalF(response); } } // This is the code that calls fbLoginStatus on initialization FB.getLoginStatus(fbLoginStatus); // This is the code that calls fbLoginStatus when there is a status change FB.Event.subscribe(’auth.statusChange’, fbLoginStatus); }; The log-in status is communicated to Main (via toSWF), and that information is used to determine whether to present the user with a log-in button. We also use that information to determine whether to display the other social graph buttons. Those buttons to check for friends and so on make sense for the application we made to show how the FB.api methods work, but having buttons for some or all those things may make no sense in your game. There will probably be other game events triggering a query of the user’s friends, for example. 467
468 Chapter 11 n Social Gaming: Social Networks The general form of an FB.api method is: function friendsF(){ FB.api(’/me/friends’, function(response) { toSWF("Friends **"); evalF(response); }); } where calling friendsF queries Facebook about the user’s friends. When testing version _01, you can see what is returned from Facebook for the 10 or so FB.api methods I included. You can use the mouse wheel to scroll output_tf so you can see all the text returned by Facebook. In the version _01 index.html code, notice the FB.login method accepts two parameters. The first is the callback function, and the second is an optional object with a scope property where you can specify some of the permissions you need when a user logs in. I included four permissions in the sample code, but that would probably cause most users to deny the permissions request. I think it would be best not to request permissions at login. Version _01 Version _01 index.html <html xmlns="http://www.w3.org/1999/xhtml" xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>game_01</title> <script language="JavaScript"> function thisMovie(movieName) { if (navigator.appName.indexOf("Microsoft") != -1) { return window[movieName]; } else { return document[movieName]; } } function toSWF(value) { thisMovie("game_01").swfF(value); } function fromSWF(value) { // If you have firebug installed in firefox, you can use // console.log to display data.
Facebook console.log("ActionScript conveys: "+value+"\n"); // Otherwise, you can use the alert function. // alert("ActionScript sends: "+value); } ///////////////////// // FB.api - graph API ///////////////////// // Albums function albumsF(){ FB.api(’/me/albums’, function(response) { toSWF(";Albums**"); evalF(response); }); } // Books function booksF(){ FB.api(’/me/books’, function(response) { toSWF("Books **"); evalF(response); }); } // Check-ins function checkInsF(){ FB.api(’/me/checkins’, function(response) { toSWF("Check-ins **"); evalF(response); }); } // Events function eventsF(){ FB.api(’/me/events’, function(response) { toSWF("Events **"); evalF(response); }); } // Feed function checkFeedF(){ FB.api(’/me/feed’, function(response) { toSWF("Checked Feed **"); evalF(response); }); } // Friends function friendsF(){ 469
470 Chapter 11 n Social Gaming: Social Networks FB.api(’/me/friends’, function(response) { toSWF("Friends **"); evalF(response); }); } // Groups function groupsF(){ FB.api(’/me/groups’, function(response) { toSWF("Groups **"); evalF(response); }); } // Likes function likesF(){ FB.api(’/me/likes’, function(response) { toSWF("Likes **"); evalF(response); }); } // Locations function locationsF(){ FB.api(’/me/locations’, function(response) { toSWF("Locations **"); evalF(response); }); } // Log in function logInF(){ FB.login(function(response) { toSWF("log in **"); evalF(response); if (response.authResponse) { toSWF("user is logged in and granted some permissions."); } else { toSWF("User cancelled login or did not fully authorize."); } }, {scope:’read_stream,publish_stream,user_events,user_notes’}); } // Movies function moviesF(){ FB.api(’/me/movies’, function(response) { toSWF("Movies **"); evalF(response); }); }
Facebook // Notes function notesF(){ FB.api(’/me/notes’, function(response) { toSWF("NOTES **"); evalF(response); }); } // Permissions function checkPermissionsF(){ FB.api(’/me/permissions’, function(response) { toSWF("Checked permissions **"); evalF(response); }); } // Photos function photosF(){ FB.api(’/me/photos’, function(response) { toSWF("Photos **"); evalF(response); }); } // Videos function videosF(){ FB.api(’/me/videos’, function(response) { toSWF("Videos **"); evalF(response); }); } // Video uploaded function videoUploadedF(){ FB.api(’/me/uploaded’, function(response) { toSWF("Video uploaded **"); evalF(response); }); } ////////////////////////////////// // FB.ui - Facebook dialogs follow ////////////////////////////////// // Prompt user to add a friend function addFriendF(){ var obj = { method: ‘friends.add’, id: "100004023276213" } 471
472 Chapter 11 n Social Gaming: Social Networks FB.ui(obj, function(response){ toSWF("add friend"); evalF(response); }); } // If your game can accept in-game payments (check the App Dashboard). // Payments documents: http://developers.facebook.com/docs/credits/build/ // Prompt user to buy something function buyF() { var obj = { method: ‘pay’, action: ‘buy_item’, // You can pass any string, but your payments_get_items must // be able to process and respond to this data. order_info: {’item_id’: ‘1a’}, dev_purchase_params: {’oscif’: true} }; FB.ui(obj, function(response){ toSWF("buy"); evalF(response); }); } // Post to feed function feedF(){ var obj = { method: ‘feed’, link: ‘http://www.kglad.com/Files/fb’, picture: ‘http://www.kglad.com/Files/fb/game_01.jpg’, name: ‘game_01 feed test’, caption: ‘game_01’, description: ‘game_01 feed dialog.’ }; FB.ui(obj, function(response){ toSWF("feed response **"); evalF(response); }); } // If something fails silently, you need to request permission // Request permissions for non-public data function permissionsF(){ var obj = { method: ‘permissions.request’, perms: ‘user_birthday,user_relationship_details,read_stream’,
Facebook display: ‘popup’ }; FB.ui(obj, function(response) { toSWF("permissions **"); evalF(response); }); } // Send a request from the current user to one or more recipients function requestF(){ var obj = { method: ‘apprequests’, message: ‘Join Me!’ } FB.ui(obj, function(response){ toSWF("request response **"); evalF(response); }); } // sendF is called by ActionScript ExternalInterface.call("sendF") function sendF(){ // The object obj is defined below. It has 3 properties: method, // name, and link. // The method property of FB.ui objects specifies which dialog is // presented to the user. Using "send" triggers a dialog that // allows users to share a link. // The recipient receives the notice in their Message inbox. var obj = { method: ’send’, name: ‘Check game_01’, link: ‘http://www.kglad.com/Files/fb’, } // The first parameter in FB.ui accepts an object, and the second // parameter is a callback function that Facebook uses to return // (sometimes) useful data (response) after the FB.ui call. FB.ui(obj,function(response){ toSWF("send response**"); // To determine what is returned, I created an evalF function // that iterates through the returned parameter. evalF(response); }); } function evalF(obj){ if(typeof obj == "object"){ for(var s in obj){ 473
474 Chapter 11 n Social Gaming: Social Networks toSWF(s); evalF(obj[s]); } } else { toSWF(obj); } } </script> </head> <body> <div id="fb-root"></div> <script> // load fb js sdk (function(d, s, id) { var js, fjs = d.getElementsByTagName(s)[0]; if (d.getElementById(id)) return; js = d.createElement(s); js.id = id; js.src = "//connect.facebook.net/en_US/all.js#xfbml=1&appId=229095550543776"; fjs.parentNode.insertBefore(js, fjs); }(document, ’script’, ‘facebook-jssdk’)); // initialize with fb js (after it loads) with your App ID window.fbAsyncInit = function() { FB.init({ appId : ‘229095550543776’, // App ID status : true, // check login status cookie : true, // enable cookies to allow the server to access the session xfbml : true // parse XFBML }); // fbLoginStatus is called when there is a login status change and // when fb js initializes function fbLoginStatus(response) { // If the user is logged in, response.status is ‘connected’ if (response.status === ‘connected’) { FB.api(’/me’, function(response) { toSWF("is connected:: "+response.status); evalF(response); }); } else { // The user is not logged in. toSWF("not connected:: "+response.status); evalF(response); } }
Facebook // This is the code that calls fbLoginStatus on initialization FB.getLoginStatus(fbLoginStatus); // This is the code that calls fbLoginStatus when there is // a status change FB.Event.subscribe(’auth.statusChange’, fbLoginStatus); }; </script> <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="game_01" width="650" height="400"> <param name="movie" value="game_01.swf" /> <param name="quality" value="high" /> <param name="bgcolor" value="#ffffff" /> <param name="allowScriptAccess" value="sameDomain" /> <embed src="game_01.swf" quality="high" bgcolor="#ffffff" width="650" height="400" name="game_01" align="middle" play="true" loop="false" quality="high" allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"> </embed> </object> </body> </html> Version _01 Main.as package com.kglad{ import flash.display.MovieClip; import flash.display.Sprite; import flash.text.TextField; import flash.events.MouseEvent; // This is needed to use ExternalInterface import flash.external.ExternalInterface; public class Main extends MovieClip { private var output_tf:TextField; private var btn:Sprite; private var tf:TextField; private var returnS:String; private var btnA:Array = ["Add Friend","Buy","Feed","Permissions","Request","Send","Albums","Books","Check Ins","Events","Check Feed","Friends","Groups","Likes","Locations","Movies","Notes","Check Permissions","Photos","Videos","Video Uploaded","Log In"]; private var nextX:int = 0; private var nextY:int = 0; 475
476 Chapter 11 n Social Gaming: Social Networks private var gapX:int = 5; public function Main() { for(var i:int=0;i<btnA.length;i++){ buttonF(i); } getChildByName(String(btnA.length-1)).visible = false; outputF(); } private function loggedInF(b:Boolean):void{ // Hides the login button if the user is logged in // and hides the social graph buttons if the user // is not logged in. getChildByName(String(btnA.length-1)).visible = !b ; for(var i:int=6;i<btnA.length-1;i++){ getChildByName(String(i)).visible = b; } } private function buttonF(i:int):void { tf = new TextField(); with(tf){ multiline=false; text=btnA[i]; autoSize="left"; } btn = new Sprite(); addChild(btn); with(btn){ name = i.toString(); mouseEnabled=true; mouseChildren = false; buttonMode = true; x=nextX; y=nextY; graphics.beginFill(0xb0b0b0); graphics.drawRect(0,0,tf.width+10,20); graphics.endFill(); } if(i==6 || btn.x+btn.width>stage.stageWidth){ btn.x = 0; nextY += 25; btn.y = nextY; nextX = btn.x+btn.width+gapX; } else { nextX += btn.width+gapX;
Facebook } tf.x = (btn.width-tf.width)/2; btn.addChild(tf); btn.addEventListener(MouseEvent.CLICK, dataSendF); } private function outputF():void { output_tf = new TextField(); output_tf.y=nextY+25; output_tf.width=stage.stageWidth-10; output_tf.height=stage.stageHeight-output_tf.y-10; output_tf.multiline=true; output_tf.wordWrap=true; output_tf.border=true; output_tf.text=""; addChild(output_tf); ExternalInterface.addCallback("swfF",this.fromJS); } private function dataSendF(e:MouseEvent):void { // stringF() just converts the strings in btnA // to match the corresponding JavaScript function // (as a string). ExternalInterface.call(stringF(btnA[int(e.currentTarget.name)])); } private function stringF(s:String):String{ returnS = s.split(" ").join("")+"F"; return returnS.substr(0,1).toLowerCase()+returnS.substr(1); } private function fromJS(value:String):void { if(value.indexOf("not connected::")>-1){ loggedInF(false); } else if(value.indexOf(" is connected::")>-1){ loggedInF(true); } output_tf.appendText("JS callback to AS:" + value + ":\n"); if(output_tf.maxScrollV>1){ output_tf.scrollV = output_tf.maxScrollV; } } } } 477
478 Chapter 11 n Social Gaming: Social Networks Adobe’s Facebook ActionScript API The Adobe Facebook ActionScript API also uses the ExternalInterface class and JavaScript to communicate with Facebook, so you still need to use SWF embedding code that is compatible with the ExternalInterface class. Adobe uses SWFObject (http://code.google.com/p/swfobject) to embed the SWF and injects the JavaScript using XML and ActionScript. You can check the FacebookJSBridge class to see how that is done. At the time of this writing, the Adobe Facebook ActionScript (version 1.8.1) code uses Mike Chambers’ JSON class. But if you are publishing for Flash Player 11 or better, you must use the native Flash JSON class. Both JSON classes are similar in that they are named the same and use static functions to convert to and from JSON format. Where Chambers’ JSON class uses JSON.decode() and JSON.encode(), the native Flash JSON class uses JSON.parse() and JSON.stringify(). So, if you use Adobe’s Facebook ActionScript API and see: 1061: Call to a possibly undefined method encode through a reference with static type Class. errors, you’ll need to replace JSON.stringify(). JSON.decode() with JSON.parse and JSON.encode() with To use Adobe’s Facebook ActionScript API, navigate to http://code.google.com/p/ facebook-actionscript-api and download the latest version of the GraphAPI documentation, examples, source code, and/or SWC files. If you’re publishing for Flash Player 11 or better, you will need to change all the JSON methods in the non-SWC files. If you use the SWC files instead of the source files, you will have less code to change. In /support files/Chapter 11/facebook/fb2 are files that use Adobe’s Facebook ActionScript API for the web. The code that uses the API is in FlashWebMain.as. With only a few changes, FlashWebMain.as (the document class of FlashWebExample.fla) is the same as Adobe’s example file with the same name. I added details (in cbF, dialogsF, and graphF) showing how to use the Facebook.ui and Facebook.api methods. FlashWebMain.as /* Copyright (c) 2010, Adobe Systems Incorporated All rights reserved.
Facebook Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Adobe Systems Incorporated nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package { import import import import import import public com.adobe.serialization.json.JSON; com.facebook.graph.Facebook; flash.display.Sprite; flash.events.MouseEvent; fl.data.DataProvider; flash.events.Event; class FlashWebMain extends Sprite { protected static const APP_ID:String = "229095550543776"; //Place your application id here public function FlashWebMain() { configUI(); cbF(); } protected function configUI():void { //listeners for UI 479
480 Chapter 11 n Social Gaming: Social Networks loginToggleBtn.addEventListener(MouseEvent.CLICK, handleLoginClick, false, 0, true); clearBtn.addEventListener(MouseEvent.CLICK, handleClearClick, false, 0, true); //Initialize Facebook library Facebook.init(APP_ID, onInit); } protected function onInit(result:Object, fail:Object):void { if (result) { //already logged in because of existing session outputTxt.text = "onInit, Logged In\n"; loginToggleBtn.label = "Log Out"; loginout_tf.text = "FB.logout"; } else { outputTxt.text = "onInit, Not Logged In\n"; } } protected function handleLoginClick(event:MouseEvent):void { if (loginToggleBtn.label == "Log In") { var opts:Object = {scope:"publish_stream, user_photos"}; Facebook.login(onLogin, opts); } else { Facebook.logout(onLogout); } } protected function onLogin(result:Object, fail:Object):void { if (result) { //successfully logged in outputTxt.text = "Logged In"; loginToggleBtn.label = "Log Out"; loginout_tf.text = "FB.logout"; } else { outputTxt.appendText("Login Failed\n"); } } protected function onLogout(success:Boolean):void { outputTxt.text = "Logged Out"; loginToggleBtn.label = "Log In"; loginout_tf.text = "FB.login"; } protected function onUICallback(result:Object):void { outputTxt.appendText("\n\nUICallback: " + JSON.stringify(result));
Facebook outputTxt.verticalScrollPosition outputTxt.maxVerticalScrollPosition ; } = protected function onCallApi(result:Object, fail:Object):void { if (result) { outputTxt.appendText("\n\nRESULT:\n" + JSON.stringify(result)); } else { outputTxt.appendText("\n\nFAIL:\n" + JSON.stringify(fail)); } outputTxt.verticalScrollPosition = outputTxt.maxVerticalScrollPosition ; } protected function handleClearClick(event:MouseEvent):void { outputTxt.text = ""; } private function cbF():void{ // dialogs_cb var dp:DataProvider = new DataProvider(); var obj:Object = {id:"100004023276213"}; dp.addItem({label:"Add Friend",data:{dialog:"friends.add",params:obj}}); obj = { action: ‘buy_item’, order_info: {’item_id’: ‘1a’}, dev_purchase_params: {’oscif’: true} }; dp.addItem({label:"Buy",data:{dialog:"pay",params:obj}}); obj = { link: ‘http://www.kglad.com/Files/fb’, picture: ‘http://www.kglad.com/Files/fb/game_01.jpg’, name: ‘game_01 feed test’, caption: ‘game_01’, description: ‘game_01 feed dialog.’ } dp.addItem({label:"Feed",data:{dialog:"feed",params:obj}}); obj = { perms: ‘user_birthday,user_relationship_details,read_stream’, display: ‘popup’ }; 481
482 Chapter 11 n Social Gaming: Social Networks dp.addItem({label:"Permissions",data:{dialog:"permissions.request", params:obj}}); obj = { message: ‘Join Me!’ } dp.addItem({label:"Request",data:{dialog:"apprequests",params:obj}}); dialogs_cb.dataProvider = dp; dialogs_cb.addEventListener(Event.CHANGE,dialogsF); // graph_cb dp = new DataProvider(); var graphA:Array = ["Albums ","Books","Checkins","Events", "Feed","Friends","Groups","Likes ", "Locations","Movies","Notes","Permissions","Photos","Videos","Uploaded"]; for(var i:int=0;i<graphA.length;i++){ dp.addItem({label:graphA[i],data:graphA[i].toLowerCase()}); } graph_cb.dataProvider = dp; graph_cb.addEventListener(Event.CHANGE,graphF); } private function dialogsF(e:Event):void{ var data:Object = dialogs_cb.selectedItem.data Facebook.ui(data.dialog, data.params, onUICallback); } private function graphF(e:Event):void{ var data:Object = graph_cb.selectedItem; Facebook.api("/me/"+graph_cb.selectedItem.data, onCallApi, {}, "GET"); } } } Twitter There are three levels of Twitter features you can add to your game. The amount of work required to add features from these levels increases as the feature level increases. The first and easiest feature level is adding one (or more) of four Twitter buttons to your embedding HTML (see Figure 11.10).
Twitter Figure 11.10 Collection of Twitter buttons that can be easily added to your embedding HTML. Source: Twitter® Inc. If you only need to use one or more of those buttons, you simply need to read the upcoming “Adding Twitter Buttons” section. The second and third feature levels require use of Twitter’s REST API (https:// dev.twitter.com/docs/api). The difference between what I call the second and third levels is that second-level REST API requests require no authentication, while thirdlevel requests require authentication. To determine whether you need to use authentication, check Twitter’s REST API. It looks as if there are fewer than 150 resource requests you can make from Twitter at the time of this writing, so it’s easy to scan the list to see what requests you need for your game. Click on each request you need to see details of the request. Toward the top on the right side is a Resource Information box that contains a Requires Authentication? listing (see Figure 11.11). Resource information box with Requires Authentication? listing, among others Figure 11.11 Each Twitter request links to a page like this, showing details about that request. Source: Twitter® Inc. 483
484 Chapter 11 n Social Gaming: Social Networks If it says Yes, you need to use at least one level-three feature, and you should read the “Requests That Require Authentication” section. If it says No or Supported, authentication is not needed for that request. If none of your requests need authentication, you should read the “Requests That Do Not Require Authentication” section. Because there is a significant difference in complexity in implementing Twitter integration among the three levels, you probably want to use the lowest level that meets your needs. Adding Twitter Buttons If you go to https://twitter.com/about/resources/buttons, you can pick one (or more) of those four buttons and generate the code needed to add them to your embedding HTML. Just follow the directions and copy and paste the rendered code into your HTML document. Share a Link Button For example, to add a Tweet button (see Share a Link in Figure 11.10) that links to http://www.kglad.com/Files/twitter and tweets “Check this”, you would use the following: <a href="https://twitter.com/share" class="twitter-share-button" dataurl="http://www.kglad.com/Files/twitter" data-text="Check this".>Tweet</a> <script> !function(d,s,id){ var js,fjs=d.getElementsByTagName(s)[0]; if(!d.getElementById(id)){ js=d.createElement(s); js.id=id; js.src="//platform.twitter.com/widgets.js"; fjs.parentNode.insertBefore(js,fjs); } }(document,"script","twitter-wjs"); </script> You should change the data-url and data-text attributes to strings appropriate for your game. You can experiment with the optional settings to see what they do. Add your Twitter button code to your embedding HTML document, positioning your button to suit your needs.
Twitter Follow Button <a href="https://twitter.com/kglad" class="twitter-follow-button" datashow-count="false">Follow @kglad</a> <script> !function(d,s,id){ var js,fjs=d.getElementsByTagName(s)[0]; if(!d.getElementById(id)){ js=d.createElement(s); js.id=id; js.src="//platform.twitter.com/widgets.js"; fjs.parentNode.insertBefore(js,fjs); } }(document,"script","twitter-wjs"); </script> Hashtag Button <a href="https://twitter.com/intent/tweet?button_hashtag=TwitterStories&text=kglad %20story" class="twitter-hashtag-button" data-related="kglad" data-url="http://www. kglad.com/Files/twitter">Tweet #TwitterStories</a> <script> !function(d,s,id){ var js,fjs=d.getElementsByTagName(s)[0]; if(!d.getElementById(id)){ js=d.createElement(s); js.id=id; js.src="//platform.twitter.com/widgets.js"; fjs.parentNode.insertBefore(js,fjs); } }(document,"script","twitter-wjs"); </script> Mention Button <a href="https://twitter.com/intent/tweet?screen_name=kglad" class="twitter-mentionbutton" data-related="kglad">Tweet to @kglad</a> <script> !function(d,s,id){ var js,fjs=d.getElementsByTagName(s)[0]; if(!d.getElementById(id)){ js=d.createElement(s); js.id=id; js.src="//platform.twitter.com/widgets.js"; fjs.parentNode.insertBefore(js,fjs); } 485
486 Chapter 11 n Social Gaming: Social Networks }(document,"script","twitter-wjs"); </script> Notice that the JavaScript for each button is identical. If you use more than one of these buttons, you need to add the JavaScript only once. Requests That Do Not Require Authentication If you want to integrate your game with Twitter, you can use the Twitter REST API at https://dev.twitter.com/docs/api. Many requests do not require authentication. These requests are easier to use than those requiring authentication. Just click on a link on https://dev.twitter.com/docs/ api, and toward the bottom, after an explanation of the parameters, is an https GET request that you can copy. Just change, add, or remove parameters to suit your needs and use that with a URLLoader. However, because of cross-domain security issues, you won’t be able to load the responses from twitter.com directly into your SWF. A cross-domain security file at twitter.com would remove the issue. In 2009, Twitter had a cross-domain file but removed it because of security issues. It doesn’t seem likely that they will add one again soon. You can circumvent the problem by having your Flash game call a same-domain executable (such as a PHP) file that does the cross-domain querying of Twitter and then returns the Twitter response to your game. That’s what I did in the /support files/ Chapter 11/twitter/no_authentication files. I used libcurl, a PHP library that allows http and https connection protocols, in gateway.php. gateway.php <?php $requestURL = $_POST[’requestURL’]; $cr = curl_init($requestURL); curl_setopt($cr, CURLOPT_RETURNTRANSFER, true); $returnS = curl_exec($cr); curl_close($cr); echo $returnS; ?> You don’t need to understand PHP to use this file, because nothing in it needs to be changed. Just upload it to your server in the same directory with your game SWF and its embedding HTML file.
Twitter All the Twitter responses can be returned in at least two formats: JSON (JavaScript Object Notation) and XML. I used JSON for no particular reason. You can use either. Requests are made using an http or https call to twitter.com with the format specified in the URL. For example, to query five favorites for kglad in JSON, use the following: https://api.twitter.com/1/favorites.json?count=5&screen_name=kglad The same query but requesting the return in XML would be the following: https://api.twitter.com/1/favorites.xml?count=5&screen_name=kglad Notice the only difference is the suffix after specified. favorites where JSON and XML are If you are publishing for Flash Player 11 or better, you can use the native Flash JSON class. Otherwise, you can use Mike Chambers’ JSON class, which you can download from https://github.com/mikechambers/as3corelib. Extract the files and either add the as3corelib.swc file to your library path or copy his com directory and files from the src directory to your game’s directory and import his JSON class. To add his SWC file to your library, click File > Publish Settings and click the ActionScript Settings icon (refer to Figure 10.1). Select the Library Path tab and click the Browse to SWC File icon (refer to Figure 10.2). Navigate to the as3corelib.swc file and click OK and then OK again. Or, if you copy his com directory to your game’s development directory, import his JSON class: import com.adobe.serialization.json.JSON; The code used to handle JSON data is slightly different depending on whether you use the native Flash JSON class or Mike Chambers’ JSON class. I will show both in the following Main.as code. Main is the document class for twitter.swf in /support files/Chapter 11/twitter/ no_authentication. If you open twitter.fla from the directory, you will see an input textfield (screenName_tf), a combobox (cb), and a textfield to show the return from Twitter (return_tf). Main.as package com.kglad{ import flash.display.MovieClip; import flash.net.URLLoader; import flash.events.Event; import flash.net.URLRequest; 487
488 Chapter 11 n Social Gaming: Social Networks import flash.net.URLRequestMethod; import flash.net.URLVariables; import flash.events.IOErrorEvent; // If you are publishing for less than Flash Player 11, use // Mike Chambers’ JSON class. Otherwise, use the native JSON // class https://github.com/mikechambers/as3corelib //import com.adobe.serialization.json.JSON; public class Main extends MovieClip { private var urlVar:URLVariables; private var urlReq:URLRequest; private var urlLoader:URLLoader; public function Main() { return_tf.text = ""; // Default Twitter screen name. screenName_tf.text = "kglad"; // Set up the combobox requestsF(); urlVar = new URLVariables(); // php file that forwards the request from the swf to // Twitter and returns Twitter’s response to the swf. urlReq=new URLRequest("gateway.php"); urlReq.method=URLRequestMethod.POST; urlLoader = new URLLoader(); urlLoader.addEventListener(Event.COMPLETE,completeF); urlLoader.addEventListener(IOErrorEvent.IO_ERROR,errorF); } private function loadF(urlS:String):void { urlVar.requestURL=urlS; urlReq.data=urlVar; urlLoader.load(urlReq); } private function requestsF():void { // Combobox items added. Several requests (none of // which required authentication) were chosen to // demonstrate how to use the REST API when // authentication is not required. cb.addItem({label:"Enter Name or ID, Make request",data:""}); cb.addItem({label:"Request Limit",data:"https://api.twitter. com/1/account/rate_limit_status.json"}); cb.addItem({label:"Contributors",data:"https://api.twitter.com/1/users/ contributors.json?screen_name=screenName&skip_status=true"});
Twitter cb.addItem({label:"Favorites",data:"https://api.twitter.com/1/favorites. json?count=5&screen_name=screenName"}); cb.addItem({label:"Followers",data:"https://api.twitter.com/1/followers/ ids.json?cursor=-1&screen_name=screenName"}); cb.addItem({label:"Friends",data:"https://api.twitter.com/1/friends/ids. json?cursor=-1&screen_name=screenName"}); cb.addItem({label:"Look Up",data:";https://api.twitter.com/1/users/lookup. json?screen_name=screenName"}); cb.addItem({label:"Retweeted to user",data:"https://api.twitter.com/1/ statuses/retweeted_to_user.json?screen_name=screenName&count=3"}); cb.addItem({label:"Search",data:"http://search.twitter.com/search.json? q=screenName&rpp=2&result_type=mixed&show_user=true"}); cb.addItem({label:"Show",data:"https://api.twitter.com/1/users/show.json? screen_name=screenName"}); cb.addItem({label:"Suggestions",data:"https://api.twitter.com/1/users/ suggestions.json"}); cb.addItem({label:"Timeline",data:"http://api.twitter.com/1/statuses/user_ timeline.json?screen_name=screenName&count=5&include_rts=1"}); cb.addItem({label:"Trends",data:"https://api.twitter.com/1/trends/daily. json"}); // Combobox listener cb.addEventListener(Event.CHANGE,cbF); } // Combobox listener function private function cbF(e:Event):void{ // The first combobox item is informational only. // No request is sent. if(cb.selectedItem.data!=""){ // If a request is sent, disable the combobox // so requests cannot be repeatedly made until // data is returned from previous request. requestEnableF(false); // Check if screen name is number (ID) or not. if(isNaN(Number(screenName_tf.text))){ // If not a number, only need to replace // screenName with the text from // screenName_tf. loadF(cb.selectedItem.data.replace("screenName",screenName_tf.text)); } else { 489
490 Chapter 11 n Social Gaming: Social Networks // If it is a number, need to replace // screenName with text from screenName_tf // and replace screen_name with user_id loadF(cb.selectedItem.data.replace("screenName",screenName_tf.text).replace ("screen_name","user_id")); } } } private function requestEnableF(b:Boolean):void{ // enable/disable cb cb.enabled = b; if(!b){ // If disabling, add message to return_tf // informing user. return_tf.appendText("Please wait. Request sent, return pending...\n******* "+cb.selectedItem.label+":\n"); } } // Request return error handler private function errorF(e:IOErrorEvent):void { // Re-enable cb requestEnableF(true); return_tf.appendText(e+"\n"+e.text); } // Request complete handler private function completeF(e:Event):void { // Re-enable cb requestEnableF(true); // If you use Mike Chambers’ JSON class, uncomment // the following line and comment out the line // that follows. //var obj:Object = JSON.decode(e.target.data); // If using the Flash Native JSON class, leave // this line uncommented. var obj:Object=JSON.parse(e.target.data); // obj is converted from JSON data to a Flash Object evalF(obj); // Scroll the TextArea to the most recent // received data. return_tf.verticalScrollPosition = return_tf.maxVerticalScrollPosition; } // Recurrsive object evaluator. You almost certainly will
Twitter // extract only a few bits of data returned in obj. You can // use evalF to determine what bits you want to use and how // to extract them without iterating through all of obj. private function evalF(obj:*) { if (obj is String | | obj is Number) { return_tf.appendText(obj+"\n"); } else { for (var s:* in obj) { return_tf.appendText("| "+s+"\n"); evalF(obj[s]); } } } } } If you upload the PHP, HTML, and SWF files to your server, you should be able to get a good idea of how to work with Twitter. With some requests (such as friends), data will be returned that contains other Twitter user IDs. You can enter an ID in screenName_tf and request more Twitter data. By using data returned from Twitter, you can piggyback requests one after the other for more sophisticated use of Twitter’s data. Unfortunately, Twitter allows a limited number of requests per hour. If you make a request on behalf of an authenticated user, the limit is per user. Without authentication, the rate limit is per IP address from which you are making the request. The limit is 150 unauthenticated requests per hour and 350 authenticated requests per hour, according to Twitter documents. Some basic tests showed that each unauthenticated request is counted by Twitter as making somewhere between 2 and 10 requests. I have no idea how they’re counting, but it’s easy to exceed their unauthenticated limit. You can use Request Limit to see how many more requests you can make before exceeding the limit, and you can see when your limit will be reset (in Greenwich Mean Time). You can also see how many requests are charged against you after making another request by checking your limit before and after making a request. Request Limit requests do not count against your limit. 491
492 Chapter 11 n Social Gaming: Social Networks Requests That Require Authentication You need to complete two main steps to use requests that require authentication. First, you must register your game with Twitter. Second, you must either implement the OAuth protocol or use a library like Tweetr that implements the protocol. Game Registration To use requests that require authentication (and those for which it is supported), you must first register your game with Twitter at https://dev.twitter.com/apps/new. (See Figure 11.12.) Figure 11.12 The Twitter application registration page at https://dev.twitter.com/apps/new. Source: Twitter® Inc. Fill out their form, agree with all those rules, prove you are a human being by filling out the Captcha form, and click the Create your Twitter Application button. You should see what looks like Figure 11.13.
Twitter Figure 11.13 After successful registration, copy your Consumer Key and Secret. Source: Twitter® Inc. Copy your Consumer Key and Consumer Secret. You will need both to make authenticated requests on behalf of users. Twitter uses these numbers to identify your game when a user gives your game permission to make requests on his behalf. Click the Settings tab (see Figure 11.14) and add an icon or change your settings if needed. In particular, note the Application Type settings. If you need more than read access, check the Read and Write or Read, Write, and Access Direct Messages options. 493
494 Chapter 11 n Social Gaming: Social Networks Figure 11.14 The Twitter application registration page after successful registration and clicking the Settings tab. Source: Twitter® Inc. Click Update This Twitter Application’s Settings if you made any changes or additions. Tweetr Tweetr is a free open-source ActionScript library that implements the OAuth protocol and allows your game to make Twitter requests that require authentication. Setting up your game to use Tweetr is a multistep process.
Twitter All the needed files are in /support files/Chapter 11/twitter/authentication. But to ensure you have the latest files, go to http://wiki.swfjunkie.com/tweetr. 1. Download and extract swcs.zip. 2. Download and extract oauth_template.zip. 3. Download and extract docs.zip. 4. Download and extract proxy.zip. 5. Add tweetrWEB swc to your library path extracted from the swc.zip. 6. Go to http://code.google.com/p/as3crypto/downloads/list and download as3crypto.swc. 7. Add as3crypto.swc to your library path. (See Figure 11.15a.) Figure 11.15a You should have both TweetrWEB.swc and as3crypto.swc in your library path, and both should have Link Type: Merged into Code. Your paths will be different from that shown. Source: Adobe Systems Incorporated. 495
496 Chapter 11 n Social Gaming: Social Networks 8. Find the js folder extracted in Step 2 and upload it to your server. 9. Find the proxy folder (that contains index.php, install.php, and Tweetr.php) extracted in Step 4 and upload to your website. 10. Open your website’s install.php file in your browser and follow the steps to create the needed files. Here is a walkthrough: http://wiki.swfjunkie.com/ tweetr:howtos:installing-the-proxy. 11. Create a verified.html page and upload it to your server. 12. Create twitter.html, your swf’s embedding HTML page, and upload it to your server. 13. Upload the swfObject folder from /support files/Chapter 11/twitter/authentication. verified.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <script language="JavaScript" type="text/JavaScript" src="js/tweetrOAuth.js"></script> </head> <body> <script type="text/JavaScript"> OAuth.verify(); </script> </body> </html> twitter.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> <head> <script language="JavaScript" type="text/JavaScript" src="js/tweetrOAuth.js"></script> <script language="JavaScript" type="text/JavaScript" src="js/swfobject.js"></script> </head> <body> <div id="flashDiv"> no flash
Twitter </div> <script type="text/JavaScript"> var so = new SWFObject("twitter.swf", "twitter", "700", "400", "9", "#336699"); so.addParam("allowScriptAccess", "always"); so.write("flashDiv"); </script> <script type="text/JavaScript"> OAuth.setFlashElement("twitter"); </script> </body> </html> You’re now ready to create your game, twitter.swf. In the following document class code, I’ll show you how to use the Tweetr class to obtain authentication and make requests that require authentication. (Warning: This code will fail because Twitter made changes that broke the code. Below I show how to fix the problem.) I think you should see how code that once worked and then was broken by Twitter can be fixed because you are likely to encounter the same issue of needing to repair code that once worked but was latter broken by changes made by Twitter. Twitter is notorious for making frequent changes that break previously working code. Main.as package com.kglad{ import com.swfjunkie.tweetr.oauth.OAuth; import com.swfjunkie.tweetr.oauth.events.OAuthEvent; import com.swfjunkie.tweetr.Tweetr; import com.swfjunkie.tweetr.data.objects.StatusData; import com.swfjunkie.tweetr.data.DataParser; import com.swfjunkie.tweetr.events.TweetEvent; import com.swfjunkie.tweetr.utils.TweetUtil; import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageScaleMode; import flash.filters.DropShadowFilter; import flash.text.StyleSheet; import flash.text.TextField; import flash.net.SharedObject; import flash.display.MovieClip; import flash.events.MouseEvent; import flash.events.Event; 497
498 Chapter 11 n Social Gaming: Social Networks import flash.utils.describeType; /** * Pinless OAuth Example AS3 * @author Sandro Ducceschi [swfjunkie.com, Switzerland] */ public class Main extends MovieClip { public var oauth:OAuth; private var tweetr:Tweetr; private var so:SharedObject; public function Main() { // Login MovieClip is visible by default. login_mc.buttonMode = true; // Make the request combobox unavailable until // authorization complete. cb.visible = false; clear_mc.visible = false; clear_mc.buttonMode = true; return_tf.text = ""; // Create or fetch a sharedobject to minimize // Twitter logins. so = SharedObject.getLocal("twitterSO"); // Initialize an OAuth instance oauth = new OAuth(); // Assign the consumerKey and ConsumerSecret // properties. You must change this to your key // and secret. oauth.consumerKey="la4QXat9lUweDKtf7Ew3g"; oauth.consumerSecret="3quoW0oEWCXlBZxROWCvaSQeAgxvIgZCaNceXlYxex0"; // Initialize a Tweetr instance, tweetr tweetr = new Tweetr(); // Assign the serviceHost of tweetr to be your // proxy directory. // You must change this to your proxy. tweetr.serviceHost="http://www.kglad.com/Files/twitter/proxy"; // Add listeners for request returns tweetr.addEventListener(TweetEvent.STATUS, requestStatusF); tweetr.addEventListener(TweetEvent.COMPLETE, requestCompleteF); tweetr.addEventListener(TweetEvent.FAILED, requestErrorF); // If a user has authorized your game in the past (and // not deleted his SharedObject), use the stored oauthToken // and oauthTokenSecret if(so.data.oauthToken){ // Retrieve and assign username, oauthToken, and
Twitter // oauthTokenSecret oauth.oauthToken = so.data.oauthToken; oauth.oauthTokenSecret = so.data.oauthTokenSecret; oauth.username = so.data.username; // Assign tweetr’s oAuth property. this.tweetr.oAuth = oauth; // Ready the request combobox tweetrF(); // Allow use of clear_mc clear_mc.visible = true; clear_mc.addEventListener(MouseEvent.CLICK,clearReturnF); } else { // Assign callbackURL, needed for the Twitter // authentication scheme when using pinless // authorization. oauth.callbackURL="http://www.kglad.com/Files/twitter/verified.html"; // You should use pinless authorization. // Pin-based authorization should only be used // for games not embedded in a web browser (like // a game installed on a mobile device or desktop). oauth.pinlessAuth=true; // Add listeners oauth.addEventListener(OAuthEvent.COMPLETE, handleOAuthEvent); oauth.addEventListener(OAuthEvent.ERROR, handleOAuthEvent); // Login button listener login_mc.addEventListener(MouseEvent.CLICK,clickF); } } private function tweetrF():void{ // Make login button unavailable login_mc.visible = false; // Make request combobox available and populate it. cb.visible = true; cb.addItem({label:"Make Request",data:""}); cb.addItem({label:"Request Limit",data:"getRateLimitStatus"}); cb.addItem({label:"Favorites",data:"getFavorites"}); cb.addItem({label:"Followers",data:"getFollowers"}); cb.addItem({label:"Friends",data:"getFriends"}); cb.addItem({label:"Mentions",data:"getMentions"}); cb.addItem({label:"Retweets by You",data:"getRetweetsByMe"}); cb.addItem({label:"Retweets of You",data:"getRetweetsOfMe"}); cb.addItem({label:"Your Details",data:"getUserDetails"}); 499
500 Chapter 11 n Social Gaming: Social Networks cb.addItem({label:"Your Home Timeline",data:"getHomeTimeLine"}); // Combobox Event.CHANGE listener cb.addEventListener(Event.CHANGE,cbF); } function cbF(e:Event):void{ // If anything other than "Make Request" is selected if(cb.selectedItem.data!=""){ return_tf.appendText("Please wait. Request sent...\n"); return_tf.verticalScrollPosition = return_tf.maxVerticalScrollPosition; // Make combobox unavailable until request // return complete cb.visible = false; if(cb.selectedItem.label=="Your Details"){ return_tf.appendText("Username| "+oauth.username+"\n"); // Check the Tweetr API. This is for the // getUserDetails method of the Tweetr class. // Both here and in the else branch, Array // notation is used to convert the // cb.selectedItem.data string // to a Tweetr method tweetr[cb.selectedItem.data](oauth.username); } else { // Array notation is used to convert the // cb.selectedItem.data string to a Tweetr // method tweetr[cb.selectedItem.data](); } } } function clickF(e:MouseEvent):void{ return_tf.appendText("Creating OAuth Authorization Window. This will launch a popup-window.\nIf your browser blocks it, allow popups and reload the page.\n"); // Request authorization token. oauth.getAuthorizationRequest(); } // return from oauth.getAuthorizationRequest(); private function handleOAuthEvent(e:OAuthEvent):void { if (e.type==OAuthEvent.COMPLETE) { // Allow use of clear_mc clear_mc.visible = true;
Twitter clear_mc.addEventListener(MouseEvent.CLICK, clearReturnF); return_tf.appendText("Authorization Successful!\n"+oauth.toString()+"\n"); // Assign oAuth property of tweetr this.tweetr.oAuth = oauth; // Save oauthToken, oauthTokenSecret, and // username to SharedObject so.data.oauthToken = oauth.oauthToken; so.data.oauthTokenSecret = oauth.oauthTokenSecret; so.data.username = oauth.username; // Ready the request combobox tweetrF(); } else { return_tf.appendText(e.text+"\n"); } } private function clearReturnF(e:MouseEvent):void{ return_tf.text = ""; } private function requestCompleteF(e:TweetEvent):void { return_tf.appendText("Complete ******\n"+e.info+"\n"); responseF(e); } private function requestStatusF(e:TweetEvent):void{ // This can be used for debugging. //return_tf.appendText("Status ******\n"+e.info+"\n"); //responseF(e); } private function requestErrorF(e:TweetEvent):void { return_tf.appendText("Error ******\n"+e.info+"\n"); responseF(e); } private function responseF(e:TweetEvent):void{ cb.visible = true; // Tweetr uses XML return data instead of JSON for // everything except trends, which is only supplied // by Twitter in JSON format. For non-trend data, I // am displaying the entire xml returned by Twitter. // You will probably want to use a small part // of what is returned: parse the return using the // methods of the Flash XML class. if(tweetr.returnType.indexOf("trends")>-1){ evalF(JSON.parse(e.data.toString())); 501
502 Chapter 11 n Social Gaming: Social Networks } else { return_tf.appendText(e.data+"******\n"); } return_tf.verticalScrollPosition = return_tf.maxVerticalScrollPosition; } private function evalF(obj:*) { if (obj is String || obj is Number) { return_tf.appendText(obj+"\n"); } else { for (var s:* in obj) { return_tf.appendText("| "+s+"\n"); evalF(obj[s]); } } } } } There is a problem. None of the trend methods (currentTrends, weeklyTrends) works. dailyTrends, trends, Even worse, after writing this code and verifying that everything (except the trend methods) was working, Twitter must’ve made some changes, because nothing was working when I reviewed this chapter. I was seeing security sandbox errors for all the requests (except the trend methods, which trigger 404 Page Not Found errors). Because SWC files contain compiled code, you cannot check or edit them (unless you decompile them). Fortunately, Tweetr is open source, and the author has made the source code available. So, if you remove the SWC from the library path and add the source files, you can use the source code. That way, you can edit the source code and correct the problems. Click File > Publish Settings > ActionScript 3.0 Settings > Library Path and click the Tweetr SWC to select it. Then click the Remove Selected Path icon (the minus sign) to remove the SWC. (See Figure 11.15b.)
Twitter Remove selected path icon Figure 11.15b Library Path panel with tweetrWEB.swc selected. Source: Adobe Systems Incorporated. Navigate to http://wiki.swfjunkie.com/tweetr:downloads and download the Tweetr source code package. Extract the files and copy the com directory to the directory that contains your game FLA. The first problem occurs when the user tries to log in and oauth.getAuthorizationRequest() executes. There is a security sandbox error thrown because the SWF in your domain is trying to load data from twitter.com. To fix that error, you need to edit the OAuth class. So, open OAuth.as in the com.swfjunkie.tweetr.oauth directory. When you do that, you will see these two CONFIG constants: 503
504 Chapter 11 n Social Gaming: Social Networks CONFIG::AIR CONFIG::WEB These are conditional compilation constants. By defining one of these constants true and the other false, you can use the same class files to publish two different versions of OAuth. To define the compilation constants, click File > Publish Settings > ActionScript 3.0 Settings > Config Constants (see Figure 11.16). Figure 11.16 The Config Constants panel, where you define conditional compilation constants. Source: Adobe Systems Incorporated. Click the Add Config Constant icon (plus sign) and change CONFIG::CONFIG_ CONST to CONFIG::AIR. Then double-click the Value column next to CONFIG::AIR
Twitter and type false. Likewise, add CONFIG::WEB and assign a value of true. Obviously, if you want to publish an AIR version, assign opposite values to those two constants. Now check the getAuthorizationRequest function. You can see urlLoader is trying to load data from twitter.com (OAUTH_DOMAIN = "http://twitter.com"). Let’s route that communication through gateway.php, just like we did with the requests that do not need authentication. In the variables section, declare urlVar and urlReq. private var urlVar:URLVariables; private var urlReq:URLRequest = new URLRequest("gateway.php"); Then change getAuthorizationRequest to: public function requestAccessToken(verifier:String):void { request=ACCESS; this.verifier=verifier; var urlRequest:URLRequest = new URLRequest(OAUTH_DOMAIN+ACCESS); urlRequest.url=_serviceHost+ACCESS+"?"+getSignedRequest("GET",urlRequest.url); urlVar.requestURL=urlRequest.url; urlReq.data=urlVar; urlLoader.load(urlReq); } If you test now, you will be able to start the authentication process, but you will be unable to finish because of another security sandbox violation. We need to edit requestAccessToken: public function requestAccessToken(verifier:String):void { request=ACCESS; this.verifier=verifier; var urlRequest:URLRequest = new URLRequest(OAUTH_DOMAIN+ACCESS); urlRequest.url=_serviceHost+ACCESS+"?"+getSignedRequest("GET",urlRequest. url); urlVar.requestURL=urlRequest.url; urlReq.data=urlVar; urlLoader.load(urlReq); } After those two functions are fixed, you should be able to complete the authentication process. And all the non-trend requests work because the SWF is accessing twitter.com via the (local) proxy files. As long as we are fixing the Tweetr files, we might as well fix the trend methods in the Tweetr class. Open Tweetr.as in com.swfjunkie.tweetr and find the trend constants: 505
506 Chapter 11 n Social Gaming: Social Networks private static const URL_TWITTER_TRENDS:String = "http://search.twitter.com/trends.json"; private static const URL_TWITTER_TRENDS_CURRENT:String = "http://search.twitter.com/trends/current.json"; private static const URL_TWITTER_TRENDS_DAILY:String = "http://search.twitter.com/trends/daily.json"; private static const URL_TWITTER_TRENDS_WEEKLY:String = "http://search.twitter.com/trends/weekly.json"; Replace them with the updated URLs from the REST API and comment out the currentTrends method (which no longer exists). private static const URL_TWITTER_TRENDS:String = "https://api.twitter.com/1/trends/ 1.json"; private static const URL_TWITTER_TRENDS_DAILY:String = "https://api.twitter.com/1/ trends/daily.json"; private static const URL_TWITTER_TRENDS_WEEKLY:String = "https://api.twitter.com/1/ trends/weekly.json"; Add trend items to the cb combobox in Main.as: private function tweetrF():void { // Make login button unavailable login_mc.visible=false; // Make request ComboBox visible and populate it. cb.visible=true; cb.addItem({label:"Make Request",data:""}); cb.addItem({label:"Request Limit",data:"getRateLimitStatus"}); cb.addItem({label:"Favorites",data:"getFavorites"}); cb.addItem({label:"Followers",data:"getFollowers"}); cb.addItem({label:"Friends",data:"getFriends"}); cb.addItem({label:"Mentions",data:"getMentions"}); cb.addItem({label:"Retweets by You",data:"getRetweetsByMe"}); cb.addItem({label:"Retweets of You",data:"getRetweetsOfMe"}); cb.addItem({label:"Current Trends",data:"trends"}); cb.addItem({label:“Daily Trends”,data:“dailyTrends”}); cb.addItem({label:“Weekly Trends”,data:“weeklyTrends”}); cb.addItem({label:“Your Details”,data:“getUserDetails”}); cb.addItem({label:“Your Home Timeline”,data:“getHomeTimeLine”}); // ComboBox Event.CHANGE listener cb.addEventListener(Event.CHANGE,cbF); } If you test now, you will trigger security sandbox errors for the trend methods. We’ll fix those exactly like we did before using gateway.php.
Twitter Replace the trend methods in the Tweetr class with: public function trends():void { _returnType=RETURN_TYPE_TRENDS_RESULTS; setGETRequest(); urlVar.requestURL=URL_TWITTER_TRENDS; urlReq.data=urlVar; urlLoader.load(urlReq); //urlLoader.load(new URLRequest(URL_TWITTER_TRENDS)); } /** * Returns the current top 10 trending topics on Twitter. */ /* public function currentTrends():void{ _returnType = RETURN_TYPE_TRENDS_RESULTS; setGETRequest(); urlLoader.load(new URLRequest(URL_TWITTER_TRENDS_CURRENT)); } */ /** * Returns the top 20 trending topics for each hour in a given day. */ public function dailyTrends():void { _returnType=RETURN_TYPE_TRENDS_RESULTS; setGETRequest(); urlVar.requestURL=URL_TWITTER_TRENDS_DAILY; urlReq.data=urlVar; urlLoader.load(urlReq); // urlLoader.load(new URLRequest(URL_TWITTER_TRENDS_DAILY)); } /** * Returns the top 30 trending topics for each day in a given week. */ public function weeklyTrends():void { _returnType=RETURN_TYPE_TRENDS_RESULTS; setGETRequest(); urlVar.requestURL=URL_TWITTER_TRENDS_WEEKLY; urlReq.data=urlVar; urlLoader.load(urlReq); // urlLoader.load(new URLRequest(URL_TWITTER_TRENDS_WEEKLY)); } Now everything should work. 507
508 Chapter 11 n Social Gaming: Social Networks There is a DataParser class that is part of Tweetr, but you will need to do error checking before passing any data to it, because it doesn’t appear that there is any error checking in DataParser. So, for example, if your game triggers a request-limit error message, you will need to check for that before using any of the DataParser methods on the returned data. Google+ Just like Facebook and Twitter, the simplest use of Google+ with a game is to add one (or more) of Google+’s buttons, such as the +1, Badge, or Share button. Adding a button requires you to add some HTML tags to the SWF’s embedding HTML file and include the https://apis.google.com/js/plusone.js file. In addition, you can customize what Google+ members see when they share your content by adding meta tags. If you go to https://developers.google.com/+/plugins, you can see samples of the Google+ buttons and snippet display. See Figure 11.17 for a partial screenshot of that page that shows the various Google+ buttons and a snippet. Figure 11.17 The Google+ +1 button, Share button, Badge button, and snippet display. Source: Google® Inc.
Google+ Adding a +1 Button To add a +1 button, add one (or more) of the following divs after the opening body tag in your HTML, followed by the JavaScript needed to include https://apis.google. com/js/plusone.js. Adding the data-annotation attribute allows for different +1 button appearances. <div class="g-plusone" data-annotation="none"></div> <!-- inline annotation. a width of 120 is minimal. if you increase the width some additional text ("Recommend this on Google") will appear after the number of recommendations. --> <div class="g-plusone" data-annotation="inline" data-width="120"></div> <!-- bubble annotation --> <div class="g-plusone"></div> <!-- asynchronous loading of https://apis.google.com/js/plusone.js so it does not delay execution of code following the +1 button code --> <script type="text/javascript"> (function() { var po = document.createElement(’script’); po.type = ’text/javascript’; po.async = true; po.src = ‘https://apis.google.com/js/plusone.js’; var s = document.getElementsByTagName(’script’)[0]; s.parentNode.insertBefore(po, s); })(); </script> Adding a Badge The Google+ Badge links directly to your profile. You need to use your Google+ ID in the data-href attribute, and you need to include https://apis.google.com/js/plusone. js. To find your Google+ ID, go to your Google+ page and click Profile. Your ID is the number in the URL between “google.com/” and “/posts.” The following div will add a Google+ Badge linking to your profile if you replace your_google_plus_id with your Google+ ID. <div class="g-plus" data-height="69" data-href="//plus.google.com/your_google_plus_id?rel=author"></div> <script type="text/javascript"> (function() { var po = document.createElement(’script’); po.type = ’text/javascript’; po.async = true; 509
510 Chapter 11 n Social Gaming: Social Networks po.src = ‘https://apis.google.com/js/plusone.js’; var s = document.getElementsByTagName(’script’)[0]; s.parentNode.insertBefore(po, s); })(); </script> Adding a Share Button The Google+ Share button allows Google+ members to share a link to your site with their friends. Again, you need to include https://apis.google.com/js/plusone.js. You can customize what members see in the shared link by using Goggle+ snippets. The following div will add a Google+ Share button. <div class="g-plus" data-action="share"></div> <script type="text/javascript"> (function() { var po = document.createElement(’script’); po.type = ’text/javascript’; po.async = true; po.src = ‘https://apis.google.com/js/plusone.js’; var s = document.getElementsByTagName(’script’)[0]; s.parentNode.insertBefore(po, s); })(); </script> To customize what Google+ members see in your shared link, you can use meta tags within your HTML file’s head tags and add itemscope and itemtype attributes to your HTML tag. For example, the following customizes the snippet’s title and description and adds an image. <meta itemprop="name" content="KGLAD TEST"> <meta itemprop="description" content="kglad test description"> <meta itemprop="image" content="kg.png"> Google+ Plug-In Summary Here is the code for all the Google+ buttons and snippet customization in the context of a SWF’s embedding HTML. There are two files with this code at /support files/ Chapter 11/googlePlus/googlePlus_01/index.html and /support files/Chapter 11/ googlePlus/googlePlus_00/oauth2callback/index.html. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/ xhtml1/DTD/xhtml1-strict.dtd"> <!-- Add itemscope and itemtype attributes to your html tag and use meta tags with itemprop attributes to assign snippets (a preview that is seen when a post contains a link to your game). -->
Google+ <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" itemscope itemtype= "http://schema.org/Product"> <head> <!-- These attributes are used in google plus snippets. --> <meta itemprop="name" content="KGLAD TEST"> <meta itemprop="description" content="kglad test description"> <meta itemprop="image" content="kg.png"> <title>index</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <style type="text/css" media="screen"> html, body { height:100%; background-color: #ffffff;} body { margin:0; padding:0; overflow:hidden; } #flashContent { width:100%; height:100%; } </style> </head> <body> <!--google plus +1 button with various annotations. You need to include the https://apis.google.com/js/plusone.js file for these buttons to appear and function --> <div class="g-plusone" data-annotation="none"></div> <br/> <!-- inline annotation. a width of 120 is minimal. if you increase the width some additional text ("Recommend this on Google") will appear after the number of recommendations. --> <div class="g-plusone" data-annotation="inline" data-width="120"></div> </br> <!-- bubble annotation --> <div class="g-plusone"></div> <br/> <!-- google plus badges (link directly to your profile). You need to include the https://apis.google.com/js/plusone.js file for these buttons to appear and function --> <!-- To find your google plus id, go to your google plus page and click profile. Your id is the number in the url between google.com/ and /posts --> <div class="g-plus" data-height="69" datahref="//plus.google.com/105564099866221383831?rel=author"></div> <br/> <!-- google plus share button. If you assign a snippet, it will be seen when a user shares your link. --> <div class="g-plus" dataaction="share"></div><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> <br/><br/><br/>–> <!-- asynchronous loading of https://apis.google.com/js/plusone.js so it does not delay execution of code following the +1 button code --> <script type="text/javascript"> (function() { var po = document.createElement(’script’); 511
512 Chapter 11 n Social Gaming: Social Networks po.type = ’text/javascript’; po.async = true; po.src = ‘https://apis.google.com/js/plusone.js’; var s = document.getElementsByTagName(’script’)[0]; s.parentNode.insertBefore(po, s); })(); </script> <div id="flashContent"> <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="800" height="400" id="index" align="middle"> <param name="movie" value="index.swf" /> <param name="quality" value="high" /> <param name="bgcolor" value="#ffffff" /> <param name="play" value="true" /> <param name="loop" value="true" /> <param name="wmode" value="window" /> <param name="scale" value="showall" /> <param name="menu" value="true" /> <param name="devicefont" value="false" /> <param name="salign value=" /> <param name="allowScriptAccess" value="sameDomain "/> <!--[if !IE]>--> <object type="application/x-shockwave-flash" data="index.swf" width="800" height="400"> <param name="movie" value="index.swf" /> <param name="quality" value="high" /> <param name="bgcolor" value="#ffffff" /> <param name="play" value="true" /> <param name="loop" value="true" /> <param name="wmode" value="window" /> <param name="scale" value="showall" /> <param name="menu" value="true" /> <param name="devicefont "value="false" /> <param name="salign" value="" /> <param name="allowScriptAccess" value="sameDomain" /> <!--<![endif]--> <a href="http://www.adobe.com/go/getflash"> <img src="http://www.adobe.com/images/shared/download_buttons/ get_flash_player.gif" alt="Get Adobe Flash player" /> </a> <!--[if !IE]>--> </object> <!--<![endif]--> </object>
Google+ </div> </body> </html> Google+ API The Google+ API is explained at https://developers.google.com/+/api. It has many similarities to the Facebook and Twitter APIs. Google+’s API is also a REST API, and Google+ (like Facebook and Twitter) also has, among others, a JavaScript library. (See Figure 11.18.) Unlike Twitter and Facebook, there is no Google+ ActionScript library at the time of this writing. Figure 11.18 Screenshot of the Google+ libraries at https://developers.google.com/+/downloads. Source: Google® Inc. First, I will show you how to use the Google+ REST API, starting with two different SWFs—one that requests an authorization token and the second that uses the authorization token to query Google+. Those files are in /support files/Chapter 11/ googlePlus/googlePlus_00. Second, I will show you how to combine the code into one SWF. Those files are in /support files/Chapter 11/googlePlus/googlePlus_01. Third, we will separate the Google+ code from the user interface code creating the Google+ library. Those files are in /support files/Chapter 11/googlePlus/googlePlus_02. Finally, we will create a Google+ SWC file from the Google+ library. The SWC and the files that use it are in /support files/Chapter 11/googlePlus/googlePlus_03. To start, you must register your game with Google. Navigate to https://code.google. com/apis/console/?pli=1# and click Create Project. (See Figure 11.19.) 513
514 Chapter 11 n Social Gaming: Social Networks Figure 11.19 Google Create Project page. Source: Google® Inc. Then select the Google+ service you want to use (the Google+ API as shown in Figure 11.20) by toggling the off button to on (Figure 11.21). Toggle google+ on Figure 11.20 Select the Google+ service. Source: Google® Inc.
Google+ Figure 11.21 Google+ toggled on. Source: Google® Inc. Click the API Project combobox (see Figure 11.22) and click Rename. Enter the name of your game and click Save. (I entered Test Game, so that’s what you’ll see after Figure 11.22.) If you have more than one project/game, you will see them listed in the combobox under Recent Projects. API project combobox Figure 11.22 Click the API Project combobox to rename (or delete) your game. Source: Google® Inc. Click the API Access tab and click Create an OAuth 2.0 Client ID. (See Figure 11.23.) 515
516 Chapter 11 n Social Gaming: Social Networks Create an OAuth 2.0 client ID button Figure 11.23 Click the API Access tab and click Create an OAuth 2.0 Client ID. Source: Google® Inc. Enter a product (game) name and, if desired, a logo. Click Next, check Web Application, and enter your host site URL after selecting http or https protocol. Finally, click Create Client ID. (See Figure 11.24.) Create client ID button Figure 11.24 After checking Web Application, enter your host site URL, select the appropriate protocol, and click Create Client ID. Source: Google® Inc.
Google+ After a brief delay, you should be back at your API Access page, and it should look something like Figure 11.25. API key Client ID Figure 11.25 API Access page after creating an OAuth ID. Source: Google® Inc. You need your client ID to retrieve an authorization token, and you use that authorization token to make Google+ requests that require authorization. You need your API key to make Google+ requests that do not require authorization. Also, note your Redirect URIs from the API Access page. If you retrieve an authorization token, that Redirect URI is called by the Google+ server. The authorization token is appended to the URI as a query string. You can retrieve the query string using the JavaScript href property of window.location in the embedding HTML. Because you need only a few lines of JavaScript to return that href property, this is a convenient place to mention JavaScript injection. JavaScript injection has been used since Flash Player 8, when the ExternalInterface class was added to the ActionScript 2.0 API. The best explanation I have seen is from Peter McBride at www.actionscript.org/resources/articles/745/1/JavaScriptand-VBScript-Injection-in-ActionScript-3/Page1.html, where both JavaScript and VisualBasic (for Internet Explorer) injection are discussed. 517
518 Chapter 11 n Social Gaming: Social Networks For the script needed to retrieve an HTML page’s address: <script> function(){ return window.location.href; } </script> we can inject it into the embedding HTML (and nicely format it) using the following ActionScript. var href_js :XML = <script> <![CDATA[ function(){ return window.location.href; } ]]> </script> var urlS:String = ExternalInterface.call(href_js); There are two sample Flash applications that use the Google+ API. The first is in /support files/Chapter 11/googlePlus/googlePlus_00 and uses two different SWF and HTML files. One pair (googlePlus.html and googlePlus.swf) requests authorization, and the other (in /support files/Chapter 11/googlePlus/googlePlus_00/oauth2callback), index.html and index.swf, is the callback URI. The second sample is in /support files/Chapter 11/googlePlus/googlePlus_01, index.html and index.swf. Both the code for the authorization request and the code to handle Google+ API requests are in index.swf. Sample 1 Code: Separate Authorization File and Request File com.kglad.Main—document class for authorization request, googlePlus.swf package com.kglad { import flash.display.MovieClip; import flash.net.URLRequest; import flash.net.URLLoader; import flash.net.URLRequestMethod; import flash.net.navigateToURL; import flash.events.Event; import flash.events.IOErrorEvent; import flash.net.URLVariables; public class Main extends MovieClip { // The http request string (assmebled in the constructor).
Google+ private var OAuthREQ:String // The http requests and responses are routed through // gateway.php to avoid cross-domain issues. private var urlR:URLRequest = new URLRequest(“gateway.php”); private var urlLoader:URLLoader = new URLLoader(); private var urlVar:URLVariables = new URLVariables; public function Main() { // Assemble the https request. The url has scope, // state, redirect uri, response type, and client id // appended as a query string OAuthREQ = “https://accounts.google.com/o/oauth2/auth?”; // To access user data, you need to specify the scope // of access. At the time of this writing, there is only // one scope for Google+. OAuthREQ += “scope=https://www.googleapis.com/auth/plus.me&”; OAuthREQ += “state=/profile&”; // This part of the query string must match the // Google+ console API Access Redirect URIs. OAuthREQ += “redirect_uri=http://www.kglad.com/Files/gp/oauth2callback&”; OAuthREQ += “response_type=token&”; // This part of the query string must match the // Google+ console API Access Client ID. OAuthREQ += “client_id=205719172028.apps.googleusercontent.com”; init(); } private function init():void{ // Add listener urlLoader.addEventListener(Event.COMPLETE,loadCompleteF); urlLoader.addEventListener(IOErrorEvent.IO_ERROR,loadErrorF); // Assign the requestURL of urlVar to OAuthREQ urlVar.requestURL = OAuthREQ; // POST the data to gateway.php, where requestURL is // parsed and used to call // https://accounts.google.com/o/oauth2/auth urlR.method = URLRequestMethod.POST; // Assign data to urlR urlR.data = urlVar; // Call gateway.php posting the https request. urlLoader.load(urlR); } private function loadCompleteF(e:Event):void{ // Should return a response from Google+ that // contains the redirect uri and access token. 519
520 Chapter 11 n Social Gaming: Social Networks // The response is html if(urlLoader.data.indexOf(’A HREF=“’)>-1){ // Extract the uri. var htmlS:String = urlLoader.data.split(’A HREF=“’)[1].split(’“’)[0]; // Convert html &amp; to & htmlS = htmlS.split(“&amp;”).join(“&”); // Open the redirect uri with query string // containing the access token and expiration // (of the access token) time. navigateToURL(new URLRequest(htmlS),"_self"); } else { trace(urlLoader.data); } } private function loadErrorF(e:Event):void{ trace(e.toString()); } } } com.kglad.Main—document class for the callback file, index.swf package com.kglad { import flash.display.MovieClip; import flash.net.URLRequest; import flash.net.URLLoader; import flash.net.URLRequestMethod; import flash.events.Event; import flash.events.IOErrorEvent; import flash.net.URLVariables; import flash.external.ExternalInterface import fl.controls.ComboBox; import flash.events.MouseEvent; public class Main extends MovieClip { // API key from Google console/API Access private const API_KEY:String = “AIzaSyBX9D4F6xmS773t31NEphlJJlJok-IZjcQ”; // Define variables private var urlR:URLRequest = new URLRequest(“gateway.php”); private var urlLoader:URLLoader = new URLLoader(); private var urlVar:URLVariables = new URLVariables; private var access_token:String; private var help_uri_obj:Object; private var resourceS:String; private var methodS:String;
Google+ public function Main() { stage.focus = input_tf; // Inject JavaScript into the swf’s embedding html file. // Used to return the embedding html’s url. The url // contains google’s access token as a query string. var href_js :XML = <script> <![CDATA[ function(){ return window.location.href; } ]]> </script> var urlS:String = ExternalInterface.call(href_js); // If not testing in Flash test environment if(urlS){ // If this is the Google+ callback with // access_toekn if(urlS.split(“access_token=”)[1]){ access_token = urlS.split(“access_token=”)[1].split(“&”)[0]; urlLoader.addEventListener(Event.COMPLETE,loadCompleteF); // Initialize User Interface initUI(); } else { trace(“Error”); } } urlR.method = URLRequestMethod.POST; urlLoader.addEventListener(IOErrorEvent.IO_ERROR,loadErrorF); } private function initUI():void{ // Initialize the comboboxes init_cbF(); // Initialize a help_uri object used to display help // and assign the uri when one of the comboboxes // changes init_help_uri_objF(); // Initialize the call API button init_callF(); // Initialize the clear button init_clearF(); clearF(); } 521
522 Chapter 11 n Social Gaming: Social Networks // the call API listener function private function APIcallF(uri:String):void{ // All the data sent to google plus is in uri urlVar.requestURL = uri; urlR.data = urlVar; urlLoader.load(urlR); } private function loadCompleteF(e:Event):void{ // Data received after calling the google plus API is // displayed in the received_ta TextArea receivedF(urlLoader.data); } private function loadErrorF(e:Event):void{ receivedF(e.toString()); } private function receivedF(s:String):void{ received_ta.appendText("******************** Begin Data ********************\n"+"****** ";+resourceS+"/"+methodS+": Input Param = "+input_tf. text+" ******\n"+s+"\n******************** End Data ********************\n"); received_ta.verticalScrollPosition = received_ta.maxVerticalScrollPosition; } private function init_cbF():void{ // Labels and change listeners added to the three comboboxes with(people_cb){ addItem({label:“Select an API Call”}); addItem({label:“get”}); addItem({label:“search”}); addItem({label:“listByActivity”}); addEventListener(Event.CHANGE,cbChangeF); } with(activities_cb){ addItem({label:“Select an API Call”}); addItem({label:“get”}); addItem({label:“list”}); addItem({label:“search”}); addEventListener(Event.CHANGE,cbChangeF); } with(comments_cb){ addItem({label:“Select an API Call”}); addItem({label:“get”}); addItem({label:“list”}); addEventListener(Event.CHANGE,cbChangeF);
Google+ } } private function cbChangeF(e:Event):void{ // Clear the Input Parameters textfield input_tf.text = “”; // The resource string is derived from the combobox name resourceS = e.currentTarget.name.split(“_”)[0]; // The method is the label as long as Select an API Call // is not selected methodS = e.currentTarget.selectedLabel; if(methodS.indexOf(“Select”)==-1){ // helpF displays help for calling a // particular API helpF(resourceS,methodS); } } // The help_uri_obj is defined. It is an Object that uses // the resource (people, activites, comments) and the method // (get, search, list, listByActivity) to store help text and // function name. private function init_help_uri_objF():void{ help_uri_obj = {}; help_uri_obj[“people”] = {}; // The second parameter is a function that returns the // uri string. This allows the most recent input_tf.text // to be used in the API calls. help_uri_obj[“people”][“get”] = ["Get a person’s profile. me (the game player) is used by default. If you want a different profile, enter their user ID (not name) in the Input Parameter textfield. You can use People/search to find ID’s using names”,peopleGetF]; help_uri_obj[“people”][“search”] = [“Search all public profiles. Enter your search term (person’s name) in the Input Parameter textfield.”,peopleSearchF]; help_uri_obj[“people”][“listByActivity”] = [“List all of the people in the specified collection for a particular activity (post) where collection = \”resharers\" or \“plusoners\”. In this example plusoners is used. Enter activity ID in the Input Parameter textfield. Find activity ID’s by using Activities/list.",peopleListByActivityF]; help_uri_obj[“activities”] = {}; help_uri_obj[“activities”][“get”] = [“Get an activity (post) by id. Enter the activity ID in the Input Parameter textfield. An activity ID can be found using Activities/list.”,activitiesGetF]; help_uri_obj[“activities”][“list”] = [“List all of the activities (posts) for a particular user. Enter the user ID in the Input Parameter textfield or the default \”me\" (the game player) will be used.",activitiesListF]; 523
524 Chapter 11 n Social Gaming: Social Networks help_uri_obj[“activities”][“search”] = [“Search public activities (posts). Enter a search string in the Input Parameter textfield.”,activitiesSearchF]; help_uri_obj[“comments”] = {}; help_uri_obj[“comments”][“get”] = [“Get a comment by ID. Find a comment ID from Comments/list and enter it in the Input Parameter textfield.”,commentsGetF]; help_uri_obj[“comments”][“list”] = [“List all of the comments for an activity (post) by ID. Enter the activity ID in the Input Parameter textfield. An activity ID can be found using Activities/list.”,commentsListF]; } private function helpF(resourceS,methodS):void{ // The first array element of help_uri_obj is the // help text displayed when a combobox changes. help_tf.text = help_uri_obj[resourceS][methodS][0]; } private function init_callF():void{ // The call API button listener call_btn.addEventListener(MouseEvent.CLICK,callF); } private function callF(e:MouseEvent):void{ // The second array element of help_uri_obj is the // function to be called and returns the // google plus uri. var uri:String = help_uri_obj[resourceS][methodS][1](); //trace(uri); APIcallF(uri); } private function init_clearF():void{ // The clear Received Data TextArea listener clear_btn.addEventListener(MouseEvent.CLICK,clearF); } private function clearF(e:MouseEvent=null):void{ received_ta.text = “”; } // The functions called in callF that return the google // plus uris. The escape function url-encodes strings // converting, for example, spaces into the url-safe %20 private function peopleGetF():String{ if(input_tf.text.length<1 || input_tf.text=="me"){ return "https://www.googleapis.com/plus/v1/people/me?access_token="+access_token; } else { return "https://www.googleapis.com/plus/v1/people/"+escape(input_tf.text)+"?key="+API_KEY; }
Google+ } private function peopleSearchF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/people?query=glad&key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/people?query="+escape(input_tf.text)+" &maxResults=20"+"&key="+API_KEY; } } private function peopleListByActivityF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/activities/z12ow5iraufyz5hgw04cc1nzenjrjvlinhg/ people/plusoners?key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/activities/"+escape(input_tf.text)+"/people/ plusoners?key="+API_KEY; } } private function activitiesGetF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/activities/z12ow5iraufyz5hgw04cc1nzenjrjvlinhg? alt=json&key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/activities/"+escape(input_tf.text)+"?alt= json&key="+API_KEY; } } private function activitiesListF():String{ if(input_tf.text.length<1 || input_tf.text=="me"){ return "https://www.googleapis.com/plus/v1/people/me/activities/public?access_token= "+access_token; } else { return "https://www.googleapis.com/plus/v1/people/"+escape(input_tf.text)+"/activities/ public?alt=json&key="+API_KEY; } } private function activitiesSearchF():String{ 525
526 Chapter 11 n Social Gaming: Social Networks if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/activities?query=help&key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/activities?query="+escape(input_tf.text)+ "&key="+API_KEY; } } private function commentsGetF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/comments/zUgUkZP-t0npF0jntaPA3G-VBTt6blguPAmJjNBI_LSktqQLLZyqTA5SViPGXU6rJ3Tzu706Zk?key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/comments/"+escape(input_tf.text)+"?key= "+API_KEY; } } private function commentsListF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/activities/z12ow5iraufyz5hgw04cc1nzenjrjvlinhg/ comments?alt=json&key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/activities/"+escape(input_tf.text)+"/comments? alt=json&key="+API_KEY; } } } } Sample 2 Code: Authorization and Requests in One File com.kglad.Main—document class for file with authorization and requests code package com.kglad { import flash.display.MovieClip; import flash.net.URLRequest; import flash.net.URLLoader; import flash.net.URLRequestMethod; import flash.net.navigateToURL; import flash.events.Event; import flash.events.IOErrorEvent; import flash.net.URLVariables;
Google+ import import import public flash.external.ExternalInterface fl.controls.ComboBox; flash.events.MouseEvent; class Main extends MovieClip { // API key from Google console/API Access private const API_KEY:String = "AIzaSyA6BBrmHCsL5trpQzj4oIXUBxAkCZwHcWU"; private var OAuthREQ:String // Define variables private var urlR:URLRequest = new URLRequest("gateway.php"); private var urlLoader:URLLoader = new URLLoader(); private var urlVar:URLVariables = new URLVariables; private var access_token:String; private var help_uri_obj:Object; private var resourceS:String; private var methodS:String; public function Main() { stage.focus = input_tf; urlR.method = URLRequestMethod.POST; // Inject JavaScript into the swf’s embedding html file. // Used to return the embedding html’s url. The url // contains google’s access token as a query string. var href_js :XML = <script> <![CDATA[ function(){ return window.location.href; } ]]> </script> var urlS:String = ExternalInterface.call(href_js); // If not testing in Flash test environment if(urlS){ // This if-else statement is the code that allows // both the initial request for an access token // and the processing of the access token to be // in one file. If this is the Google+ callback // with access_token if(urlS.split(“access_token=”)[1]){ access_token = urlS.split(“access_token=”)[1].split(“&”)[0]; urlLoader.addEventListener(Event.COMPLETE,loadCompleteF); // Initialize User Interface initUI(); } else { // If this is initial load of page, // request access_token. 527
528 Chapter 11 n Social Gaming: Social Networks OAuthREQ = “https://accounts.google.com/o/oauth2/auth?”; //OAuthREQ += “scope=https://www.googleapis.com/auth/userinfo.email+https://www.googleapis.com/ auth/urlshortener+https://www.googleapis.com/auth/plus.me+https://www.googleapis. com/auth/userinfo.profile&”; //OAuthREQ += “scope=https://www.googleapis.com/auth/userinfo.email+https://www.googleapis.com/ auth/userinfo.profile&”; OAuthREQ += “scope=https://www.googleapis.com/auth/plus.me&”; OAuthREQ += “state=/profile&”; OAuthREQ += “redirect_uri=http://www.kglad.com/Files/gp/&”; OAuthREQ += “response_type=token&”; OAuthREQ += “client_id=187572201816.apps.googleusercontent.com”; urlLoader.addEventListener(Event.COMPLETE, initLoadCompleteF); // Initialize authorization request .initAuth(); } } urlLoader.addEventListener(IOErrorEvent.IO_ERROR,loadErrorF); } private function initAuth():void{ urlVar.requestURL = OAuthREQ; urlR.data = urlVar; urlLoader.load(urlR); } private function initLoadCompleteF(e:Event):void{ if(urlLoader.data.indexOf(’A HREF=“’)>-1){ var htmlS:String = urlLoader.data.split(’A HREF=“’)[1] .split(’”’)[0]; htmlS = htmlS.split(“&amp;”).join(“&”); navigateToURL(new URLRequest(htmlS),"_self"); } else { trace(urlLoader.data); } } private function initUI():void{ // Initialize the comboboxes init_cbF(); // Initialize a help_uri object used to display help // and assign the uri when one of the comboboxes // changes
Google+ init_help_uri_objF(); // Initialize the call API button init_callF(); // Initialize the clear button init_clearF(); clearF(); } // the call API listener function private function APIcallF(uri:String):void{ // All the data sent to google plus is in uri urlVar.requestURL = uri; urlR.data = urlVar; urlLoader.load(urlR); } private function loadCompleteF(e:Event):void{ // Data received after calling the google plus API is // displayed in the received_ta TextArea receivedF(urlLoader.data); } private function loadErrorF(e:Event):void{ receivedF(e.toString()); } private function receivedF(s:String):void{ received_ta.appendText(“******************** Begin Data ********************\n"+"****** "+resourceS+"/"+methodS+": Input Param = "+input_tf. text+" ******\n“+s+”\n******************** End Data ********************\n”); received_ta.verticalScrollPosition = received_ta.maxVerticalScrollPosition; } private function init_cbF():void{ // Labels and change listeners added to the // three comboboxes with(people_cb){ addItem({label:“Select an API Call”}); addItem({label:“get”}); addItem({label:“search”}); addItem({label:“listByActivity”}); addEventListener(Event.CHANGE,cbChangeF); } with(activities_cb){ addItem({label:“Select an API Call”}); addItem({label:“get”}); addItem({label:“list”}); addItem({label:“search”}); addEventListener(Event.CHANGE,cbChangeF); 529
530 Chapter 11 n Social Gaming: Social Networks } with(comments_cb){ addItem({label:“Select an API Call”}); addItem({label:“get”}); addItem({label:“list”}); addEventListener(Event.CHANGE,cbChangeF); } } private function cbChangeF(e:Event):void{ // Clear the Input Parameters textfield input_tf.text = “”; // The resource string is derived from the combobox name resourceS = e.currentTarget.name.split(“_”)[0]; // The method is the label as long as Select an API Call // is not selected methodS = e.currentTarget.selectedLabel; if(methodS.indexOf(“Select”)==-1){ // helpF displays help for calling a // particular API helpF(resourceS,methodS); } } // The help_uri_obj is defined. It is an Object that uses // the resource (people, activites, comments) and the method // (get, search, list, listByActivity) to store help text and // function name. private function init_help_uri_objF():void{ help_uri_obj = {}; help_uri_obj[“people”] = {}; // The second parameter is a function that returns the // uri string. This allows the most recent input_tf.text // to be used in the API calls. help_uri_obj[“people”][“get”] = [“Get a person’s profile. me (the game player) is used by default. If you want a different profile, enter their user ID (not name) in the Input Parameter textfield. You can use People/search to find ID’s using names”,peopleGetF]; help_uri_obj[“people”][“search”] = [“Search all public profiles. Enter your search term (person’s name) in the Input Parameter textfield.”,peopleSearchF]; help_uri_obj[“people”][“listByActivity”] = [“List all of the people in the specified collection for a particular activity (post) where collection = \”resharers\” or \“plusoners\”. In this example plusoners is used. Enter activity ID in the Input Parameter textfield. Find activity ID’s by using Activities/list.”, peopleListByActivityF]; help_uri_obj[“activities”] = {};
Google+ help_uri_obj[“activities“][”get“] = [”Get an activity (post) by id. Enter the activity ID in the Input Parameter textfield. An activity ID can be found using Activities/list.”,activitiesGetF]; help_uri_obj[“activities”][“list”] = [“List all of the activities (posts) for a particular user. Enter the user ID in the Input Parameter textfield or the default \”me\“ (the game player) will be used.”,activitiesListF]; help_uri_obj[“activities”][“search”] = [“Search public activities (posts). Enter a search string in the Input Parameter textfield.”,activitiesSearchF]; help_uri_obj[“comments”] = {}; help_uri_obj[“comments”][“get”] = [“Get a comment by ID. Find a comment ID from Comments/list and enter it in the Input Parameter textfield.”,commentsGetF]; help_uri_obj[“comments”][“list”] = [“List all of the comments for an activity (post) by ID. Enter the activity ID in the Input Parameter textfield. An activity ID can be found using Activities/list.”,commentsListF]; } private function helpF(resourceS,methodS):void{ // The first array element of help_uri_obj is the // help text displayed when a combobox changes. help_tf.text = help_uri_obj[resourceS][methodS][0]; } private function init_callF():void{ // The call API button listener call_btn.addEventListener(MouseEvent.CLICK,callF); } private function callF(e:MouseEvent):void{ // The second array element of help_uri_obj is the // function to be called and returns the google // plus uri. var uri:String = help_uri_obj[resourceS][methodS][1](); //trace(uri); APIcallF(uri); } private function init_clearF():void{ // The clear Received Data TextArea listener clear_btn.addEventListener(MouseEvent.CLICK,clearF); } private function clearF(e:MouseEvent=null):void{ received_ta.text = “”; } // The functions called in callF that return the google // plus uris. the escape function url-encodes strings // converting, for example, spaces into the url-safe %20 private function peopleGetF():String{ 531
532 Chapter 11 n Social Gaming: Social Networks if(input_tf.text.length<1 || input_tf.text=="me"){ return "https://www.googleapis.com/plus/v1/people/me?access_token="+access_token; } else { return "https://www.googleapis.com/plus/v1/people/"+escape(input_tf.text)+"?key="+API_KEY; } } private function peopleSearchF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/people?query=glad&key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/people?query="+escape(input_tf.text)+ "&maxResults=20"+"&key="+API_KEY; } } private function peopleListByActivityF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/activities/z12ow5iraufyz5hgw04cc1nzenjrjvlinhg/ people/plusoners?key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/activities/"+escape(input_tf.text)+"/people/ plusoners?key="+API_KEY; } } private function activitiesGetF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/activities/z12ow5iraufyz5hgw04cc1nzenjrjvlinhg? alt=json&key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/activities/"+escape(input_tf.text)+"?alt= json&key="+API_KEY; } } private function activitiesListF():String{ if(input_tf.text.length<1 || input_tf.text=="me"){ return "https://www.googleapis.com/plus/v1/people/me/activities/public?access_token= "+access_token; } else {
Google+ return "https://www.googleapis.com/plus/v1/people/"+escape(input_tf.text)+"/activities/ public?alt=json&key="+API_KEY; } } private function activitiesSearchF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/activities?query=help&key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/activities?query="+escape(input_tf.text)+ "&key="+API_KEY; } } private function commentsGetF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/comments/zUgUkZP-t0npF0jntaPA3G-VBTt6blguPAmJjNBI_LSktqQLLZyqTA5SViPGXU6rJ3Tzu706Zk?key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/comments/"+escape(input_tf.text)+"?key= "+API_KEY; } } private function commentsListF():String{ if(input_tf.text.length<1){ return "https://www.googleapis.com/plus/v1/activities/z12ow5iraufyz5hgw04cc1nzenjrjvlinhg/ comments?alt=json&key="+API_KEY; } else { return "https://www.googleapis.com/plus/v1/activities/"+escape(input_tf.text)+"/comments? alt=json&key="+API_KEY; } } } } Creating a Google+ ActionScript 3.0 Library In /support files/Chapter 11/googlePlus/googlePlus_02 you’ll find the files that separate code for the user interface and code used with Google+. The code used with Google+ is in the com.kglad.GooglePlus class and the three classes (People, Activities, and Comments) on which GooglePlus depends. 533
534 Chapter 11 n Social Gaming: Social Networks I created the library starting with the files in /support files/Chapter 11/googlePlus/ googlePlus_02 and separating user interface code from code needed to interact with Google+. The com.kglad.GooglePlus class is a singleton class. Instantiate a using: GooglePlus instance var gp:GooglePlus = GooglePlus.getInstance(api_key,client_id,redirect_uri); You must pass your API key. If you need an authentication token, you must pass a client ID. If you pass a client ID and do not pass a redirect URI, the string in the address bar is used as the redirect URI. That won’t be a problem if your redirect URI includes a file name other than index. Otherwise, that will be a problem (because Google expects an exact match), so you should explicitly pass a third parameter if you are requesting an authorization token. If there isn’t an exact match, Google will return a 400 Bad Request error. (See Figure 11.26 400 Bad Request return from Google when passing http://www.kglad.com/Files/gp/ for the redirect URI when Google expected http://www.kglad.com/Files/gp. (Note: the only difference between the two is an unexpected final forward slash.) Source: Google® Inc. Figure 11.26.) The class has eight main methods (peopleGetF, peopleSearchF, peopleList activitiesListF, activitiesSearchF, commentsGetF, commentsListF). There are eight auxillary methods (peopleGet_helpF, peopleSearch_ peopleListByActivity_helpF, activitiesGet_helpF, activitiesList_helpF, helpF, activitiesSearch_helpF, commentsGet_helpF, commentsList_helpF) that return a string containing helpful information about using corresponding main methods. GooglePlus ByActivityF, activitiesGetF,
Google+ Figure 11.27 peopleGetF method signature. ® Source: Google Inc. Figure 11.28 peopleSearchF Source: Google® Inc. method signature. 535
536 Chapter 11 n Social Gaming: Social Networks Figure 11.29 peopleListByActivityF method signature. Source: Google® Inc. Figure 11.30 activitiesGetF Source: Google® Inc. method signature.
Google+ Figure 11.31 activitiesListF method signature. Source: Google® Inc. Figure 11.32 activitiesSearchF Source: Google® Inc. method signature. 537
538 Chapter 11 n Social Gaming: Social Networks Figure 11.33 commentsGetF method signature. Source: Google® Inc. Figure 11.34 commentsListF method signature. Source: Google® Inc. Figures 11.27 through 11.34 show the eight main methods. The help documents are in /support files/Chapter 11/googlePlus/googlePlus_02/ GooglePlus/docs. Open index.html in your browser and click GooglePlus. All the available methods are listed. Here is the code used by index.swf in /support files/Chapter 11/googlePlus/ googlePlus_02 that utilizes GooglePlus.swc. The only GooglePlus code is in the Main
Google+ constructor, where GooglePlus.getInstance() is used, and in public methods of GooglePlus are used. help_objF, where all the com.kglad.Main package com.kglad { import flash.display.MovieClip; import flash.events.Event; import flash.events.MouseEvent; public class Main extends MovieClip { private var help_obj:Object; private var methodS:String; private var resourceS:String; private var gp:GooglePlus; public function Main() { //var gp:GooglePlus = GooglePlus.getInstance(api_key,client_id,redirect_uri); // The first parameter passed to getInstance is your // API key and is required. // The second parameter passed to getInstance is your // client ID and is optional. If not passed, no // authorization token will be requested. // The third parameter passed to getInstances is the // redirect URI and is optional. If no second parameter // is passed, there is no need for the redirect URI. // If a second parameter is passed and no third parameter // is passed, the URL in the address bar is used for the // redirect. If that is not exactly what Google+ expects, // you will receive a 400 Bad Request page from Google. gp = GooglePlus.getInstance("AIzaSyA6BBrmHCsL5trpQzj4oIXUBxAkCZwHcWU,187572201816.apps. googleusercontent.com,http://www.kglad.com/Files/gp/"); initUI(); } private function initUI():void{ // Initialize the comboboxes init_cbF(); // Initialize the call API button init_callF(); // Initialize the clear button init_clearF(); help_objF(); clearF(); } private function receivedF(s:String):void{ 539
540 Chapter 11 n Social Gaming: Social Networks received_ta.appendText("******************** Begin Data ********************\n"+"****** "+resourceS+"/"+methodS+": Input Param = "+input_tf. text+" ******\n"+s+"\n******************** End Data ********************\n"); received_ta.verticalScrollPosition = received_ta.maxVerticalScrollPosition; } private function init_cbF():void{ // Labels and change listeners added to the // three comboboxes with(people_cb){ addItem({label:"Select an API Call"}); addItem({label:"get"}); addItem({label:"search"}); addItem({label:"listByActivity"}); addEventListener(Event.CHANGE,cbChangeF); } with(activities_cb){ addItem({label:"Select an API Call"}); addItem({label:"get"}); addItem({label:"list"}); addItem({label:"search"}); addEventListener(Event.CHANGE,cbChangeF); } with(comments_cb){ addItem({label:"Select an API Call"}); addItem({label:"get"}); addItem({label:"list"}); addEventListener(Event.CHANGE,cbChangeF); } } private function cbChangeF(e:Event):void{ // Clear the Input Parameters textfield input_tf.text = ""; // The resource string is derived from the combobox name resourceS = e.currentTarget.name.split("_")[0]; // The method is the label as long as Select an API Call // is not selected methodS = e.currentTarget.selectedLabel; if(methodS.indexOf("Select")==-1){ // helpF displays help for calling a // particular API helpF(resourceS,methodS); } }
Google+ private function help_objF():void{ help_obj = {}; help_obj["people"] = {}; // The second parameter is a function that returns // the uri string. This allows the most recent // input_tf.text to be used in the API calls. help_obj["people"]["get"] = [gp.peopleGet_helpF,gp.peopleGetF]; help_obj["people"]["search"] = [gp.peopleSearch_helpF,gp.peopleSearchF]; help_obj["people"]["listByActivity"] = [gp.peopleListByActivity_helpF,gp.peopleListByActivityF]; help_obj["activities"] = {}; help_obj["activities"]["get"] = [gp.activitiesGet_helpF,gp.activitiesGetF]; help_obj["activities"]["list"] = [gp.activitiesList_helpF,gp.activitiesListF]; help_obj["activities"]["search"] = [gp.activitiesSearch_helpF,gp.activitiesSearchF]; help_obj["comments"] = {}; help_obj["comments"]["get"] = [gp.commentsGet_helpF,gp.commentsGetF]; help_obj["comments"]["list"] = [gp.commentsList_helpF, gp.commentsListF]; } private function helpF(resourceS:String,methodS:String):void{ help_tf.text = help_obj[resourceS][methodS][0](); } private function init_callF():void{ // The call API button listener call_btn.addEventListener(MouseEvent.CLICK,callF); } private function callF(e:MouseEvent):void{ help_obj[resourceS][methodS][1](receivedF,input_tf.text) } private function init_clearF():void{ // The clear Received Data TextArea listener clear_btn.addEventListener(MouseEvent.CLICK,clearF); } private function clearF(e:MouseEvent=null):void{ received_ta.text = ""; } } 541
542 Chapter 11 n Social Gaming: Social Networks } Here is the source code for the GooglePlus class and three classes (People, and Comments) it depends upon. Activities, Com.kglad.GooglePlus package com.kglad{ import flash.net.URLRequest; import flash.net.URLLoader; import flash.net.URLRequestMethod; import flash.net.navigateToURL; import flash.events.Event; import flash.events.IOErrorEvent; import flash.net.URLVariables; import flash.external.ExternalInterface import flash.display.MovieClip; public class GooglePlus extends MovieClip{ private static var gp:GooglePlus; private static var allowInstantiation:Boolean; private var API_KEY:String; private var client_id:String; private var redirect_uri:String; private var urlR:URLRequest = new URLRequest("gateway.php"); private var urlLoader:URLLoader = new URLLoader(); private var urlVar:URLVariables = new URLVariables; private var OAuthREQ:String; private var callbackF:Function; private var helpS:String; private var people:People; private var activities:Activities; private var comments:Comments; public static function getInstance(API_KEY:String,client_id:String="", redirect_uri:String=""):GooglePlus { if (gp == null) { allowInstantiation=true; gp = new GooglePlus(API_KEY,client_id,redirect_uri); allowInstantiation=false; } return gp; } public function GooglePlus(_API_KEY:String,_client_id:String,_redirect_ uri:String):void {
Google+ if (! allowInstantiation) { throw new Error("Error: Instantiation failed: SingletonDemo.getInstance() instead of new."); } else { API_KEY = _API_KEY; client_id = _client_id; redirect_uri = _redirect_uri; people = new People(API_KEY); activities = new Activities(API_KEY); comments = new Comments(API_KEY); if(client_id){ getAuthF(); } } } private function getAuthF():void{ urlR.method = URLRequestMethod.POST; // Inject JavaScript into the swf’s embedding html file. // Used to return the embedding html’s url. The url // contains google’s access token as a query string. var href_js :XML = <script> <![CDATA[ <function(){ return window.location.href; } ]]> </script> var urlS:String = ExternalInterface.call(href_js); // If not testing in Flash test environment if(urlS){ // If this is the Google+ callback with // access_token if(urlS.split(“access_token=”)[1]){ var access_token:String = urlS.split(“access_token=”)[1].split(“&”)[0]; people.access_tokenF = access_token; activities.access_tokenF = access_token; urlLoader.addEventListener(Event.COMPLETE,loadCompleteF); } else { // If this is initial getInstance call and // client_id passed, request access_token. if(client_id){ // if no redirect_uri was passed, Use 543
544 Chapter 11 n Social Gaming: Social Networks // use current url. if(!redirect_uri){ redirect_uri = urlS; } OAuthREQ = “https://accounts.google.com/o/oauth2/auth?”; OAuthREQ += “scope=https://www.googleapis.com/auth/plus.me&”; OAuthREQ += “state=/profile&”; OAuthREQ += “redirect_uri=“+redirect_uri+”&”; OAuthREQ += “response_type=token&”; OAuthREQ += “client_id=”+client_id; urlLoader.addEventListener(Event.COMPLETE,authLoadCompleteF); // Initialize authorization request. initAuth(); } } } urlLoader.addEventListener(IOErrorEvent.IO_ERROR,loadErrorF); } private function initAuth():void{ //urlVar.requestURL = REQ1+API_KEY; urlVar.requestURL = OAuthREQ; urlR.data = urlVar; urlLoader.load(urlR); } private function authLoadCompleteF(e:Event):void{ if(urlLoader.data.indexOf(’A HREF=“’)>-1){ var htmlS:String = urlLoader.data.split(’A HREF=“’)[1].split(’“’)[0]; htmlS = htmlS.split(“&amp;”).join(“&”); navigateToURL(new URLRequest(htmlS),”_self”); } else { trace(urlLoader.data); } } // the call API listener function private function APIcallF(uri:String):void{ // All the data sent to google plus is in uri urlVar.requestURL = uri; urlR.data = urlVar; urlLoader.load(urlR); }
Google+ private function loadCompleteF(e:Event):void{ // Data received after calling the google plus API // is returned to the callback function. callbackF(urlLoader.data); } private function loadErrorF(e:Event):void{ trace(e.toString()); } //////////// people ////////////////// // get public function peopleGet_helpF():String{ return people.get_helpF(); //return “Get a person’s profile. me (the game player) // is used by default. If you want a different profile, // enter their user ID (not name) in the Input Parameter // textfield. You can use People/search to find ID’s // using names”; } public function peopleGetF(_callbackF:Function,queryS:String=“”):void{ callbackF = _callbackF; APIcallF(people.getF(queryS)); } // search public function peopleSearch_helpF():String{ return people.search_helpF(); } public function peopleSearchF(_callbackF:Function,queryS:String=“”, languageS:String=“”,maxResults:int=10):void{ callbackF = _callbackF; APIcallF(people.searchF(queryS,languageS,maxResults)); } // list by activity public function peopleListByActivity_helpF():String{ return people.listByActivity_helpF(); } public function peopleListByActivityF(_callbackF:Function,activityID:String=“”,collectionS: String=“plusoners”,maxResults:int=20):void{ callbackF = _callbackF; APIcallF(people.listByActivityF(activityID,collectionS,maxResults)); } //////////// activities ////////////////// // get 545
546 Chapter 11 n Social Gaming: Social Networks public function activitiesGet_helpF():String{ return activities.get_helpF(); } public function activitiesGetF(_callbackF:Function,activityID: String=“”):void{ callbackF = _callbackF; APIcallF(activities.getF(activityID)); } // list public function activitiesList_helpF():String{ return activities.list_helpF(); } public function activitiesListF(_callbackF:Function,userID:String=“”, maxResults:int=20):void{ callbackF = _callbackF; APIcallF(activities.listF(userID,maxResults)); } // search public function activitiesSearch_helpF():String{ return activities.search_helpF(); } public function activitiesSearchF(_callbackF:Function,queryS:String=“”, languageS:String=“”,maxResults:int=10,orderByS:String=“recent”):void{ callbackF = _callbackF; APIcallF(activities.searchF(queryS,languageS,maxResults,orderByS)); } //////////// comments ////////////////// // get public function commentsGet_helpF():String{ return comments.get_helpF(); } public function commentsGetF(_callbackF:Function,commentID:String=“”):void{ callbackF = _callbackF; APIcallF(comments.getF(commentID)); } // list public function commentsList_helpF():String{ return comments.list_helpF(); } public function commentsListF(_callbackF:Function,activityID:String=“”, maxResults:int=20,sortOrderS:String=“ascending”):void{ callbackF = _callbackF; APIcallF(comments.listF(activityID,maxResults,sortOrderS)); }
Google+ } } com.kglad.People package com.kglad { internal class People { private var API_KEY:String; private var access_token:String; private var uriS:String; public function People(_API_KEY:String) { API_KEY = _API_KEY; } // get internal function get_helpF():String{ return “Get a person’s profile. me (the game player) is used by default. If you want a different profile, pass their user ID (not name) to the peopleGetF method of your GooglePlus method. You can use the peopleSearchF method to find ID’s using names”; } internal function getF(queryS:String):String{ if(!queryS || queryS==“me”){ uriS = “https://www.googleapis.com/plus/v1/people/me?access_token=”+access_token; } else { uriS = “https://www.googleapis.com/plus/v1/people/”+escape(queryS)+“?key=”+API_KEY; } return uriS; } // search internal function search_helpF():String{ return “Search all public profiles. Pass your search term (person’s name) to the peopleSearchF method. You can also pass optional language and maximum results (default = 10) parameters.”; } private function searchParamsF(uriS:String,languageS:String, maxResults:int):String{ if(languageS){ uriS = uriS+“&language=”+languageS; } if(maxResults!=10){ uriS = uriS+“&maxResults=”+maxResults; } 547
548 Chapter 11 n Social Gaming: Social Networks return uriS; } internal function searchF(queryS:String,languageS:String,maxResults:int):String{ uriS = “https://www.googleapis.com/plus/v1/people?query= ”+escape(queryS)+“&maxResults=20”+“&key=”+API_KEY; uriS = searchParamsF(uriS,languageS,maxResults); return uriS; } // listByActivity internal function listByActivity_helpF():String{ return “List all of the people in the specified collection for a particular activity/post. Pass the activity ID (use the activitiesListF method to find activity IDs), an optional collection parameter (either \“resharers\” or \“plusoners\”, default = \“plusoners\”) and an optional maximum results parameter (default = 20).”; } private function listByActivityParamsF(uriS:String,collectionS: String,maxResults:int):String{ if(collectionS!=“plusoners”){ uriS = uriS+“&collection=”+collectionS; } if(maxResults!=20){ uriS = uriS+“&maxresults=”+maxResults; } return uriS; } internal function listByActivityF(activityID:String,collectionS:String,maxResults:int):String{ uriS = “https://www.googleapis.com/plus/v1/activities/”+escape (activityID)+“/people/”+collectionS+“?key=”+API_KEY; uriS = listByActivityParamsF(uriS,collectionS,maxResults); return uriS; } internal function set access_tokenF(_access_token:String):void{ access_token = _access_token } } } com.kglad.Activities package com.kglad {
Google+ internal class Activities { private var API_KEY:String; private var access_token:String; private var uriS:String; public function Activities(_API_KEY:String) { API_KEY = _API_KEY; } // get internal function get_helpF():String{ return “Get an activity (post) by id. Pass the activity ID to the activitiesGetF method. An activity ID can be found using the activitiesListF method.”; } internal function getF(activityID:String):String{ return “https://www.googleapis.com/plus/v1/activities/ “+escape(activityID)+”?alt=json&key=”+API_KEY; } // list internal function list_helpF():String{ return “List all of the activities/posts for a particular user. Pass the user ID to the activitiesListF method or the default \“me\” (the game player) will be used. An optional maximum results (default = 20) parameter may also be passed.”; } private function listParamsF(uriS:String,maxResults:int):String{ if(maxResults!=20){ uriS = uriS+“&maxresults=”+maxResults; } return uriS; } internal function listF(userID:String,maxResults:int):String{ if(!userID || userID==“me”){ uriS = “https://www.googleapis.com/plus/v1/people/me/ activities/public?access_token=”+access_token; } else { uriS = “https://www.googleapis.com/plus/v1/people/ “+escape(userID)+”/activities/public?alt=json&key=”+API_KEY; } uriS = listParamsF(uriS,maxResults); return uriS; } // search internal function search_helpF():String{ return “Search public activities/posts. Pass a search string to the activitiesSearchF method. Option parameters language, maximum results (default = 549
550 Chapter 11 n Social Gaming: Social Networks 10) and order by (\“best\” or \“recent\” are acceptable, default = \“recent\”) may also be passed.”; } private function searchParamsF(uriS:String,languageS:String, maxResults:int,orderByS:String):String{ if(languageS){ uriS = uriS+“&language=”+languageS; } if(maxResults!=10){ uriS = uriS+“&maxResults=”+maxResults; } if(orderByS!=“recent”){ uriS = uriS+“&orderBy”+orderByS; } return uriS; } internal function searchF(queryS:String,languageS:String,maxResults: int,orderByS:String):String{ if(!queryS){ uriS = “https://www.googleapis.com/plus/v1/activities? query=help&key=”+API_KEY; } else { uriS = “https://www.googleapis.com/plus/v1/activities? query=”+escape(queryS)+“&key=”+API_KEY; } uriS = searchParamsF(uriS,languageS,maxResults,orderByS); return uriS; } internal function set access_tokenF(_access_token:String):void{ access_token = _access_token } } } Com.kglad.Comments package com.kglad { internal class Comments { private var API_KEY:String; private var uriS:String; public function Comments(_API_KEY:String) { API_KEY = _API_KEY; } // get internal function get_helpF():String{
Google+ return “Get a comment by ID. Find a comment ID by calling the commentsListF method.”; } internal function getF(commentID:String):String{ uriS = “https://www.googleapis.com/plus/v1/comments/”+escape (commentID)+“?key=”+API_KEY; return uriS; } // list internal function list_helpF():String{ return “List all of the comments for an activity (post) by ID. Pass the activity ID to the activitiesListF method. Optional parameters maximum results (default = 20) and sort order (\“ascending\” - oldest first or \“descending\” - newest first, default = \“ascending\”) may also be passed. An activity ID can be found using the activitiesListF method.”; } private function listParamsF(uriS:String,maxResults:int,sortOrderS: String):String{ if(maxResults!=20){ uriS = uriS+“&maxResults=”+maxResults; } if(sortOrderS!=“ascending”){ uriS = uriS+“&sortOrder”+sortOrderS; } return uriS; } internal function listF(activityID:String,maxResults:int,sortOrderS: String):String{ uriS = “https://www.googleapis.com/plus/v1/activities/”+escape (activityID)+“/comments?alt=json&key=”+API_KEY; uriS = listParamsF(uriS,maxResults,sortOrderS); return uriS; } } } Creating a Google+ SWC from the Google+ ActionScript 3.0 Library In /support files/Chapter 11/googlePlus/googlePlus_03/GooglePlus you’ll find GooglePlus.swc, which contains the compiled code from the Google+ ActionScript 3.0 library in /support files/Chapter 11/googlePlus/googlePlus_02. To create an SWC in Flash Pro CS6, create a new FLA (for example, swcFLA.fla) and save it in the directory with access to the classes you want to compile into an SWC. 551
552 Chapter 11 n Social Gaming: Social Networks In that FLA, create an empty MovieClip (for example, by clicking Insert > New Symbol > MovieClip), check Export for ActionScript, assign the class you want to compile into the SWC (for example, com.kglad.GooglePlus), and click OK. Finally, right-click the Library MovieClip, click Export SWC File, assign your SWC’s file name (for example, GooglePlus.swc), and click Save. You now have a convenient way to share and use your code. The only thing left to do is to create documentation for your library. I used Ortelius (http://ortelius.marten.dk/sider/as-documentation-generator_27.aspx). To use this SWC, add it to your SWF’s library path (File > Publish Settings > Flash > ActionScript Settings > Library path). You can then use the public methods of this GooglePlus class. The source files are in /support files/Chapter 11/googlePlus/googlePlus_03/GooglePlus/ src. The help documents are in /support files/Chapter 11/googlePlus/googlePlus_03/ GooglePlus/docs. Open index.html in your browser and click GooglePlus. All the available methods will be listed.
Chapter 12 Social Gaming: Multiplayer Games With multiplayer games, data needs to be communicated among the players. When a player makes a move (changing the game state), the updated game state needs to be communicated to all the other players. In addition, that communication needs to occur in a timely manner. With turn-based games (such as card games), that communication among players can take as long as a few seconds without degrading the game experience. With real-time games (such as shooter games), even a 250-millisecond delay in communicating game state leads to a significantly degraded player experience. Consequently, you need substantial expertise to successfully develop and deploy real-time multiplayer games. There are two fundamentally different ways to accomplish communication among players. Players can communicate via a server (in server-based games), or they can communicate directly from player to player (in peer-to-peer games). Server-Based Multiplayer Games Generally, the code in each player’s Flash game handles the player’s input, transmits player data to the server, receives other players’ data, and displays the game state. The server receives player data, validates the data, updates and maintains game state, and transmits each player’s data to the other players. The code used on the server cannot be ActionScript, so you will need to learn a server-side coding language, such as PHP or C#. Server-side coding is beyond the scope of this book, so I won’t cover server-based multiplayer games except to say 553
554 Chapter 12 n Social Gaming: Multiplayer Games that you need to have advanced coding skills in at least two languages (ActionScript and a server-side language) to create these game types. Peer-to-Peer Games Since Flash Player 10, you can create multiplayer games without needing an intermediary server to facilitate player communication. The Flash Player can use a protocol (Adobe’s Real-Time Media Flow Protocol) that allows direct peer-to-peer communication. Instead of using server-side code to handle the game logic and coordinate game state among players, each peer in the network handles its own game logic and game state and communicates that directly to its peers, and each peer updates its game state based on the data received from others. To use peer-to-peer networking, each peer must connect with an Adobe server. Peerto-peer communication doesn’t go through that server (or it would not be peerto-peer), but peers must stay connected with the Adobe server to communicate with each other. Note: This does not use Adobe Media Server. The peer-to-peer communication is done via the Adobe cirrus server (http://labs.adobe.com/wiki/index.php/Cirrus:FAQ). To communicate with the Adobe server, you should use your own server URL and developer key. You can obtain that URL and key at www.adobe.com/cfusion/entitlement/ index.cfm?e=cirrus. Following is a simple tic-tac-toe game that uses Adobe’s peer-to-peer networking to pair up players. The NetConnection class establishes a connection to the Adobe server, while the NetGroup class is used for peer-to-peer communication. I used only a small part of the NetGroup methods for the tic-tac-toe game, but there are more available if you’re sharing data among many users and/or sharing large amounts of data. The tic-tac-toe game is in /support files/Chapter 11/multiplayer and is extensively commented. Main contains the game logic code and the code that allows each player to pair with an opponent and send and receive data from their opponent. Main.as package com.kglad{ import flash.display.MovieClip; import flash.events.MouseEvent; import flash.display.Sprite;
Peer-to-Peer Games import import import public flash.utils.getDefinitionByName; flash.utils.Timer; flash.events.TimerEvent; class Main extends MovieClip { private var obj:Object = {}; private var messageObj:Object; private var initX:int = 10; private var initY:int = 100; private var markerP:Sprite; private var boardP:Sprite; private var MarkerS:String; // Booleans used to pair group members. private var hsInitialAccept:Boolean; private var startGameCalled:Boolean private var refuseConnections:Boolean; private var hs1Timer:Timer; private var hs1ReceiveTime:int = 0; private var gameA:Array = []; private var markerCountA:Array; private var newGameTimer:Timer; public function Main() { // Timer used to delay starting a new game after // previous completed. newGameTimer = new Timer(3000,1); newGameTimer.addEventListener(TimerEvent.TIMER, startF); // Timer used to repeatedly attempt to connect with // another peer. At each step of the pairing attempt, // the timer is reset to wait some time (10 seconds is // plenty of time) for the next response. hs1Timer = new Timer(10000,1); hs1Timer.addEventListener(TimerEvent.TIMER,hs1F); // Initialize peer-to-peer (P2P) connection. Except // for peer-to-peer connection housekeeping, all // P2P received data are sent to receivedDataF // below P2P.init(this); // Initialize tic-tac-toe board setUpF(); // Initialize sendName_btn and sendMessage_btn btnsF(); // Clear carriage returns from the textfields. message_tf.text = “”; sendMessage_tf.text =“”; chat_tf.text = “”; 555
556 Chapter 12 n Social Gaming: Multiplayer Games } private function setUpF():void{ // Create a parent for the markers (X’s and O’s) markerP = new Sprite(); // Create a parent for the tic-tac-toe board boardP = new Sprite(); // If the board (when it is enabled) is clicked, // add a marker (X or O) boardP.addEventListener(MouseEvent.CLICK,markerF); boardP.buttonMode = true; boardP.mouseChildren = false; boardP.mouseEnabled = false; addChild(boardP); addChild(markerP); // Add Square instances to boardP for(var col:int = 0;col<3;col++){ for(var row:int = 0;row<3;row++){ var sq:Square = new Square(); boardP.addChild(sq); sq.col = col; sq.row = row; sq.x = initX+sq.width*col; sq.y = initY+sq.height*row; } } } private function initGameAF():void{ // gameA contains a zero if there’s no marker at the // corresponding tic-tac-toe board position. // This is used to determine when a game is won; // i.e., if there are three of this peer’s markers // in a row, column, or diagonal. gameA = []; for(var row:int=0;row<3;row++){ gameA[row] = [0,0,0]; } } private function btnsF():void{ sendName_btn.addEventListener(MouseEvent.CLICK,sendNameF); sendName_btn.buttonMode = true; sendName_btn.mouseChildren = false; // When at least one other peer connects, // sendName_btn is made visible. A peer must // send their name before they are allowed
Peer-to-Peer Games // to join another peer and start a game. sendName_btn.visible = false; sendMessage_btn.addEventListener(MouseEvent.CLICK,sendMessageF); sendMessage_btn.buttonMode = true; sendMessage_btn.mouseChildren = false; // When game starts, sendMessage_btn is made // visible, allowing peer to chat with their // paired peer. sendMessage_btn.visible = false; disconnect_btn.addEventListener(MouseEvent.CLICK,disconnectF); disconnect_btn.buttonMode = true; disconnect_btn.mouseChildren = false; // When game starts, disconnect_btn is made visible, // allowing user to disconnect. disconnect_btn.visible = false; } private function sendNameF(e:MouseEvent):void{ // disable sendName_btn if enough text is entered // into name_tf and send initial data trying to // pair up with another peer. if(name_tf.text.length>1){ // Assign Data.nameS. Data.nameS = name_tf.text // Cannot change name. sendName_btn.visible = false; message_tf.text = “Please wait. Opponent being sought. This may take a few minutes.\n” // The first call (by this peer) to hs1F // (handshake1 starts the conversation to // pair up and start a game). hs1F(); } else { textF(“You must enter more than one character into the name field”,message_tf); } } private function disconnectF(e:MouseEvent):void{ // An intentional disconnect bans further connections // between this peer and the disconnected peer. Data.bannedAddressF = Data.connectionAddressF; // checkGroupLostF is also called when a group member // disconnects (by closing their game). If the // disconnected peer was the peer with whom this // peer was paired, the pairing process is 557
558 Chapter 12 n Social Gaming: Multiplayer Games // restarted. checkGroupLostF(Data.connectionAddressF); } internal function textF(txt:String,tf:TextField):void { // stripF removes initial and trailing white space. tf.appendText(stripF(txt)+“\n”); tf.scrollV = tf.maxScrollV; } // hs1F (handshake 1 function) where initial data is // sent trying to pair up with another peer. internal function hs1F(e:TimerEvent=null):void{ if(Data.nameS.length>1 && !startGameCalled){ if(e){ // When hs1F is called by the hs1Timer // listener (i.e., when e is not null), // a different peer (if there is one in // the group) is selected to receive // the message. Data.connectionAddressUpdateF(); } // hsInitialAccept is used to allow/disallow // responses to received hs1 and hs2 data. // I am only responding to one or the other // (whichever is received first). hsInitialAccept = true; startGameCalled = false; refuseConnections = false; // messageObjF creates a new Object (thereby // eliminating data associated with any // previous messageObj). The NetGroup class // has methods that transmit objects (and // their properties). messageObj = messageObjF(); // Each messageObj has a type, which the recipient // uses to determine what type of message they are // receiving. messageObj.typeS = “hs1”; // Some messageObjs have a value. This one does not. // P2P.sendDataF sends messageObj if(Data.connectionAddressF.length>0){ // Direct this message to one group member. messageObj.destination = Data.connectionAddressF; P2P.sendDirectedDataF(messageObj); }
Peer-to-Peer Games // Start timer. If no response to this message // is received within 10 seconds, hs1TimerResetF(10000); } } // When intentionally disconnecting from a paired // user and when a connection is lost, // checkGroupLostF is called. internal function checkGroupLostF(s:String):void{ if(Data.connectionAddressF == s){ // No longer connected to paired user, // remove game markers and restart the // pairing polling. removeMarkersF(); sendMessage_btn.visible = false; startGameCalled = false; refuseConnections = false; textF(“Your opponent disconnected. Searching for new opponent”,message_tf); Data.connectionAddressUpdateF(); hs1F(); } } // All messageObj instances start with a base of data // returned by messageObjF private function messageObjF():Object{ obj = new Object(); // Each obj has an identifiable sender and // possibly recipient (Data.connectionIDF). // Data.id is an (almost) unique identifier // for each user. Data.connectionIDF is the // connected user’s Data.id. obj.id = Data.id; // Data.connectionIDF returns 0 before a // tentative pairing is made. obj.connectionID = Data.connectionIDF; // User’s name is also sent. This is not unique // and is not part of the handshake process. obj.nameS = Data.nameS; return obj; } // The hs1Timer is set/reset here. private function hs1TimerResetF(n:int):void{ if(!startGameCalled){ 559
560 Chapter 12 n Social Gaming: Multiplayer Games hs1Timer.reset(); hs1Timer.delay = n; hs1Timer.start(); } } // The function that receives all (except the disconnect // messages) the non-housekeeping data from P2P. internal function receiveDataF(obj:Object):void{ // when CONFIG::DEBUG is true, all the data in // obj is displayed in message_tf CONFIG::DEBUG{ textF(“****** receiveDataF begin: ******”,message_tf); stringifyF(obj); textF(“****** receiveDataF end: ******”,message_tf); } // Do not respond to any message unless a valid // name has been stored in Data.nameS if(Data.nameS.length<1){ return; } // When a game is started with another peer (i.e., // a pairing is completed), refuseConnections is true. // This prevents this peer from responding to all // messages unless they are from the paired // peer (i.e., when obj.from=Data.connectionAddressF). if(refuseConnections && obj.from != Data.connectionAddressF){ messageObj = messageObjF(); // hs0 is a connection refusal. messageObj.typeS = “hs0”; CONFIG::DEBUG{ textF(“hs0 sent to address:”+obj.from,message_tf); } // Direct message to sender messageObj.destination = obj.from; P2P.sendDirectedDataF(messageObj); return; } switch(obj.message.typeS){ // hs0 received. case “hs0”: // If an hs0 is received from // Data.connectAddressF, try // another peer. if(obj.from==Data.connectionAddressF){
Peer-to-Peer Games hs1TimerResetF(100); } break; case “hs1”: // hs1 received. CONFIG::DEBUG{ textF(“hs1 message received from address: “+obj.from+”. name: ”+obj.message.nameS,message_tf); } if(hsInitialAccept && Data.nameS.length>1){ // Accept hs1 or hs2, whichever // arrives first after Data.nameS // is assigned (which is also // when this peer sent an hs1). hsInitialAccept = false; // Allow 10 seconds for response // to this about-to-be-sent hs2 // message. hs1TimerResetF(10000); // Tentatively pair-up with this // hs1 sending peer. Data.connectionAddressF = obj.from; Data.connectionIDF = obj.message.id; Data.foeNameS = obj.message.nameS; // Must initialize messageObj // here and not before the Data // assignments. (Check // messageObjF to see if // Data.connectionIDF is used.) messageObj = messageObjF(); messageObj.destination = obj.from; messageObj.typeS = “hs2”; // “yes” is sent if hs1 accepted and // Data.nameS has been assigned. messageObj.valueS = “yes”; } else { messageObj = messageObjF(); messageObj.destination = obj.from; messageObj.typeS = “hs2”; // Otherwise, hsInitialAccept remains // true, no tentative pairing is made, // and “no” is sent. messageObj.valueS = “no”; } 561
562 Chapter 12 n Social Gaming: Multiplayer Games P2P.sendDirectedDataF(messageObj); break; case “hs2”: if(hsInitialAccept){ if(obj.message.valueS==“yes”){ // The hs2 sender has made // a tentative pairing with // this peer. hsInitialAccept = false; hs1TimerResetF(10000); Data.connectionAddressF = obj.from; Data.connectionIDF = obj.message.id; Data.foeNameS = obj.message.nameS; // Again, must initialize // messageObj here and not // before the Data assignments. messageObj = messageObjF(); messageObj.destination = obj.from; messageObj.typeS = “hs3”; // Confirm pairing messageObj.valueS = “yes”; } else { messageObj = messageObjF(); // Confirm rejection messageObj.valueS = “no”; hs1TimerResetF(10000); } } else { messageObj = messageObjF(); messageObj.destination = obj.from; messageObj.typeS = “hs2”; // Reject pairing messageObj.valueS = “no”; } P2P.sendDirectedDataF(messageObj); break; case “hs3”: if(obj.message.valueS==“yes”){ // Confirm pairing messageObj = messageObjF(); messageObj.destination = obj.from; messageObj.typeS = “hs4”; hs1TimerResetF(10000); P2P.sendDirectedDataF(messageObj);
Peer-to-Peer Games } else { hs1TimerResetF(10000); } break; case “hs4”: hs1TimerResetF(10000); break; // The messages below relate to chatting and // game play. Obviously, most of this coding // is to facilitate peer pairing. // A chat message. case “message”: textF(obj.message.nameS+“: ”+obj.message.valueS,chat_tf); break; // An opponent move case “move”: // Update the board so this peer’s board // reflects opponent’s move. positionMarkerF(obj.message.valueS); // Allow this peer to make move boardP.mouseEnabled = true; textF(“It is your move.”,message_tf); break; case “tie game”: // Tie game. textF(“Tie Game!\nStarting next game.”,message_tf); // Switch X and O so players alternate // who goes first. switchXO(); // Start the new game after 3-second delay. newGameTimer.start(); break; case “loss”: textF(“You lost!\nStarting next game.”,message_tf); switchXO(); newGameTimer.start(); break; } CONFIG::DEBUG{ 563
564 Chapter 12 n Social Gaming: Multiplayer Games textF(obj.message.id+“==?”+Data.connectionIDF+” :: ”+obj.message.connectionID+“==?”+Data.id+” . name=“+obj.message.nameS+”. bool: “+startGameBoolF(obj),message_tf); textF((obj.message.id==Data.connectionIDF)+” :: “+(obj.message.connectionID==Data.id)+” . name length>1:“+(obj.message.nameS.length>1)+”. startGameBoolF: “+startGameBoolF(obj),message_tf); } // Final part of pairing process done in // startGameBoolF if(!startGameCalled && startGameBoolF(obj)){ // Only want to do this once per pairing startGameCalled = true; // And start game (for first time). initStartF(obj); } } // Check if message sender is this peer’s tentative pair. private function startGameBoolF(obj:Object):Boolean{ if(obj.message.id==Data.connectionIDF && obj.message.connectionID==Data.id){ // If yes, tentative pair is no longer // tentative. This peer is paired and // will refuse communication attempts // by peers other than their paired peer. refuseConnections = true; return true; } else { return false; } } // Some preliminaries for the first time a game is started. // After a game is complete, startF() is called for // subsequent game starts. private function initStartF(obj:Object):void{ // No need to check for another peer for // possible pairing. hs1Timer.stop(); // Make the send message (chat) and disconnect // buttons visible. sendMessage_btn.visible = true; disconnect_btn.visible = true; // Smallest id is X and moves first. Both players // will agree on who has the smaller id. if(Data.id<Data.connectionIDF){
Peer-to-Peer Games MarkerS = “X”; } else { MarkerS = “O”; } message_tf.text = “You have an opponent: “+Data.foeNameS+”\n”; // Start game. startF(); CONFIG::DEBUG{ textF(“START GAME”,message_tf); } } // Send a chat message. private function sendMessageF(e:MouseEvent):void{ if(sendMessage_tf.text.length>1){ messageObj = messageObjF(); messageObj.typeS = “message”; messageObj.valueS = sendMessage_tf.text; sendMessage_tf.text = “”; textF(Data.nameS+“: ”+messageObj.valueS,chat_tf); P2P.sendDataF(messageObj); } } // Called when the board is clicked and a marker needs // to be added. private function markerF(e:MouseEvent):void{ // Check which board square was clicked. for(var i:int = 0;i<boardP.numChildren;i++){ if(boardP.getChildAt(i).hitTestPoint(boardP.mouseX,boardP.mouseY)){ var sq:MovieClip = MovieClip(boardP.getChildAt(i)); // Position a marker over that square. positionMarkerF([MarkerS,sq.x,sq.y]); // Update gameA, which is used to determine // if this peer has won (in gameOverCheckF). // Each peer checks if they have won after // their own move. gameA[sq.row][sq.col] = 1; break; } } // Disallow adding another marker (until the next turn). boardP.mouseEnabled = false; // Send a “move” message indicating this peer’s // move so opponent can update their board. messageObj = messageObjF(); 565
566 Chapter 12 n Social Gaming: Multiplayer Games messageObj.typeS = “move”; messageObj.valueS = [MarkerS,boardP.getChildAt(i).x,boardP.getChildAt(i).y]; messageObj.destination = Data.connectionAddressF; P2P.sendDirectedDataF(messageObj); // Check if game is over. if(!gameOverCheckF()){ textF(“Please wait for “+Data.foeNameS+”’s move”,message_tf); } } // This is called when opponent sends a “move” message // and when this player clicks the board to place a // marker. private function positionMarkerF(a:Array):void{ // marker string (“X” or “O“). var mS:String = a[0]; // x and y positions var xPos:int = a[1]; var yPos:int = a[2]; // Get a class reference from the marker string var ClassRef:Class = Class(getDefinitionByName(mS)); // Create a new marker of the correct class (X or O). var marker:MovieClip = new ClassRef(); // Add the marker to the marker parent. markerP.addChild(marker); // And position it. marker.x = xPos; marker.y = yPos; } // Check for a won or tie game. private function gameOverCheckF():Boolean{ // No sense checking for a won game before there // are 5 markers. if(markerP.numChildren>=5){ // Count how many of this player’s markers // are in the first row, second row, third // row, first column, second column, // third column, top-left to bottom-right // diagonal, top-right to bottom-left // diagonal. If 3 markers in any of these // rows, columns, or diagonals, this // peer wins. markerCountA=[0,0,0,0,0,0,0,0];
Peer-to-Peer Games for (var col:int=0; col<3; col++) { // Top-left to bottom-right diagonal markerCountA[6]+=gameA[col][col]; // Top-right to bottom-left diagonal markerCountA[7]+=gameA[col][2-col]; for (var row:int=0; row<3; row++) { // Row totals. markerCountA[row]+=gameA[row][col]; // Column totals. markerCountA[3+col] += gameA[row][col]; } // Check if 3 markers in any one row // or column or diagonal if(markerCountA.indexOf(3)>-1){ textF(“You won!\nStarting next game.”,message_tf); // Let opponent know they lost. messageObj = messageObjF(); messageObj.typeS = “loss”; P2P.sendDataF(messageObj); // switch X and O. switchXO(); // Start the next game. newGameTimer.start(); return true; } } } // If this player did not win and there are nine // markerP children, this is a tie game. if(markerP.numChildren==9){ textF(“Tie Game!\nStarting next game.”,message_tf); // Let opponent know tie game. messageObj = messageObjF(); messageObj.typeS = “tie game”; P2P.sendDataF(messageObj); // switch X and O. switchXO(); // Start the next game. newGameTimer.start(); return true; } return false; } 567
568 Chapter 12 n Social Gaming: Multiplayer Games // Switch X and O private function switchXO():void{ if(MarkerS==“X“){ MarkerS=“O“; } else { MarkerS=“X“; } } // Start the game private function startF(e:TimerEvent=null):void{ // Initialize gameA (used to determine winner // in gameOverCheckF); initGameAF(); // Remove markers from previous game (if there // was a previous game). removeMarkersF(); // X always goes first. if(MarkerS==“X“){ textF(“You go first and are X. Make your move.”,message_tf); boardP.mouseEnabled = true; } else { textF(Data.foeNameS+” goes first. You are O. Please wait for “+Data.foeNameS+“’s move.”,message_tf); boardP.mouseEnabled = false; } } // Remove all markers. private function removeMarkersF():void{ for(var i:int=markerP.numChildren-1;i>=0;i–){ markerP.removeChildAt(i); } } // Strip leading and trailing white space. private function stripF(s:String):String { if(s.length==0){ return s; } while (s.indexOf(” “)==0 || s.indexOf(“\n“)==0 || s.indexOf(“\r“)==0 || s.indexOf(“\t“)==0) { s=s.substr(1); } while (s.lastIndexOf(” “)==s.length-1 || s.lastIndexOf(“\n“)==s.length-1 || s.lastIndexOf(“\r“)==s.length-1 || s.lastIndexOf(“\t“)==s.length-1) {
Peer-to-Peer Games s=s.substr(0,−1); } return s; } // This is used for debugging only internal function stringifyF(obj:Object):void{ for(var s:* in obj){ if(obj[s] is String){ textF(“obj[“+s+“]=“+obj[s],message_tf); } else { textF(s+“:”,message_tf); stringifyF(obj[s]); } } } } } The P2P class establishes the peer-to-peer connection. The NetGroup class is essential to establishing an Adobe peer-to-peer connection. P2P.as package com.kglad{ import flash.display.MovieClip import flash.events.NetStatusEvent; import flash.net.NetConnection; import flash.net.NetGroup; import flash.net.GroupSpecifier; import flash.net.NetGroupReplicationStrategy; public class P2P { private static var nc:NetConnection; private static var netGroup:NetGroup; private static var main:MovieClip; internal static function init(_main:MovieClip):void { main = _main; // Create a NetConnection instance nc = new NetConnection(); // Add event listener to handle connections and data // to and from this peer. nc.addEventListener(NetStatusEvent.NET_STATUS,netStatusF); // Use your server address and key (from Adobe, //http://www.adobe.com/cfusion/entitlement/index.cfm?e=cirrus) 569
570 Chapter 12 n Social Gaming: Multiplayer Games nc.connect(Data.SERVER+Data.DEVKEY); } private static function netStatusF(e:NetStatusEvent):void { CONFIG::DEBUG { main.textF(“p2p netStatusF begin:”,main.message_tf); main.stringifyF(e.info); main.textF(“p2p netStatusF end:”,main.message_tf); } switch (e.info.code) { case “NetConnection.Connect.Success“: // When this user connects, initialize group initializeGroupF(); break; case “NetGroup.Neighbor.Connect“: // only ~14 closest peers will be neighbors Data.addToGroup(e.info.neighbor); CONFIG::DEBUG{ main.textF(“*** addToGroup: ***“+e.info.neighbor,main.message_tf); } // Notify user they need to enter a name. if(main.message_tf.text.indexOf(“To start, enter your name and click SEND“)==-1 && Data.nameS.length<1){ main.textF(“To start, enter your name and click SEND”,main.message_tf); main.sendName_btn.visible = true; } break; case “NetGroup.Neighbor.Disconnect“: // A peer connection was lost. Remove from // Data.groupAddressA and check if peer was // this user’s opponent. Data.removeFromGroup(e.info.neighbor); main.checkGroupLostF(e.info.neighbor); break; case “NetGroup.SendTo.Notify“: CONFIG::DEBUG{ main.textF(“*** NOTIFY *** “+Data.groupAddressA.length,main.message_tf); } // Process received data in Main. main.receiveDataF(e.info); // Allowing netGroup.sendToNearest() to // execute crashes the debug flash player.
Peer-to-Peer Games // I could not find a problem commenting // out this section. but I only // tested with up to 40 peers. /* if(e.info.fromLocal){ main.receiveDataF(e.info); } else { netGroup.sendToNearest(e.info.message, e.info.message.destination); } */ break; } } private static function initializeGroupF():void { // Define a GroupSpecifier. Presumably, using this // should allow each developer (with his one key) // to develop many different games and applications. // This is the tic-tac-toe group. var groupSpecifier:GroupSpecifier=new GroupSpecifier(“tttGroup“); // Enable posting to entire group. groupSpecifier.postingEnabled = true; // Enable direct routing of a peer. groupSpecifier.routingEnabled = true; // Allow peers to join the group automatically. groupSpecifier.serverChannelEnabled = true; // Create NetGroup instance. netGroup=new NetGroup(nc,groupSpecifier.groupspecWithAuthorizations()); netGroup.addEventListener(NetStatusEvent.NET_STATUS,netStatusF); } internal static function sendDataF(obj:Object):void { if(Data.connectionAddressF.length>0){ // communicate only with the peer at // Data.connectionAddressF obj.destination = Data.connectionAddressF; netGroup.sendToNearest(obj, obj.destination); } else { netGroup.sendToAllNeighbors(obj); } } internal static function sendDirectedDataF(obj:Object):void { // Send message to only one peer. netGroup.sendToNearest(obj, obj.destination); 571
572 Chapter 12 n Social Gaming: Multiplayer Games } // I don’t need this function, but it shows how to send // a message to everyone in the group. internal static function sendDataToAllF(obj:Object):void{ netGroup.sendToAllNeighbors(obj); } } } The Data class is used to store and furnish data needed to establish the peer-to-peer connection and identify the opponent. Data.as package com.kglad { public class Data { // These two variables/constants you should get at: // http://www.adobe.com/cfusion/entitlement/index.cfm?e=cirrus internal static const SERVER:String=“rtmfp://p2p.rtmfp.net/fc7d4c8286fa66c1f44daccd-c751a3a86d49/“; internal static const DEVKEY:String=“fc7d4c8286fa66c1f44daccdc751a3a86d49“; // Array of connected addresses. One will be used to pair. internal static var groupAddressA:Array = []; // Address of pair connection. private static var connectionAddress:String = “”; // id of pair connection. private static var connectionID:Number = 0; // Banned or disconnected addresses. private static var bannedAddressA:Array = []; // This member’s identifier. Each group member will have // an almost unique id internal static var id:int = Math.floor(1000000000000*Math.random()); // index of tentative or actual pair private static var index:int; // This user’s name internal static var nameS:String = “”; // Paired (actual or tentative) member’s name. internal static var foeNameS:String = “”; // Used to add addresses to groupAddressA. Called from P2P // when new connection is received. internal static function addToGroup(s:String):void{ // Add s to groupAddressA if it is not in // bannedAddressA if(bannedAddressA.indexOf(s)==−1){
Peer-to-Peer Games groupAddressA.push(s); if(!connectionAddress){ index = 0; connectionAddress = groupAddressA[0]; } } } // Remove an address. Called from P2P when a connection // is lost. internal static function removeFromGroup(s:String):void{ var i:int = groupAddressA.indexOf(s); groupAddressA.slice(i,1); if(groupAddressA.length==0){ connectionAddress = “”; } } // Find another tentative connectionAddress, if possible. internal static function connectionAddressUpdateF():void{ if(groupAddressA.length>1){ index = (index+1)%groupAddressA.length; connectionAddress = groupAddressA[index]; } else if(groupAddressA.length==1){ index = 0; connectionAddress = groupAddressA[0]; } else { index = 0; connectionAddress = “”; } foeNameS = “”; connectionID = 0; } // getters, setters internal static function get connectionAddressF():String{ return connectionAddress } internal static function set connectionAddressF(s:String):void{ if(bannedAddressA.indexOf(s)==−1){ connectionAddress = s; index = groupAddressA.indexOf(s); } } internal static function get connectionIDF():int{ return connectionID; } 573
574 Chapter 12 n Social Gaming: Multiplayer Games internal static function set connectionIDF(n:int):void{ connectionID = n; } // Banned addresses. These are addresses from which // this user disconnected. internal static function set bannedAddressF(s:String):void{ // Add s to bannedAddressA bannedAddressA.push(s); // Remove s from groupAddressA for(var i:int=groupAddressA.length-1;i>=0;i– –){ if(groupAddressA[i]==s){ groupAddressA.splice(i,1); break; } } } } }
Appendix a Errors That Trigger an Error Message Use this appendix to look up error messages that you encounter. This appendix contains information that supplements the Adobe Help file appendices. 1009: Cannot Access a Property or Method of a Null Object Reference This is the most common error posted on the Adobe Flash-related forums. Fortunately, most of these errors are quick and easy to fix (assuming you read the section on permit debugging). If there is only one object reference in the problematic line of code, you can conclude that object does not exist when that line of code executes. For example, if there is no object mc on-stage, var mc:MovieClip; mc.x = 0; is the simplest code that would trigger a 1009 error. The variable mc has been declared but is null because no MovieClip exists, and it has not been created. Trying to access the x property (or any other property of mc) will result in a 1009 error because you cannot access a property of a null (or nonexistent) object. To remedy this problem, you must create the referenced object either on-stage or with ActionScript. To create the object with code, use the new constructor: var mc:MovieClip = new MovieClip(); mc.x = 0; 575
576 Appendix A n Errors That Trigger an Error Message Precisely the same error will occur in many different guises. Here’s the same error trying to reference a null object method: var s:Sound; s.play(); If there’s more than one object in the problematic line of code—for example: mc1.x = mc2.width+mc2.x; use the trace() function to find which objects are null: trace(mc1); trace(mc2); mc1.x = mc2.width+mc2.x; Of course, typos are every coder’s burden and are a common cause of 1009 errors. Instead of comparing two names and trying to confirm that they are the same (for example, an instance name in the Properties panel and an ActionScript name), it’s more reliable to copy the name from one location and paste it to the other. Then you can be certain the names are the same. There are, however, more obtuse ways to trigger a 1009 error when you create and delete objects on-stage and especially if you use timeline tweening. I have seen some 1009 errors that are impossible to diagnose without checking the History panel. And if that panel is cleared or the problem was created far enough in the past that the steps causing the error are no longer present in the History panel, there’s no way (known to me) to deduce what caused the error. However, you can still fix those errors. If you’re certain you have an on-stage object whose instance name (in the Properties panel) matches your ActionScript reference and it is in a frame that plays no later than your code referencing that object, you may need to take a few steps backward to solve the problem. But first make sure that frame plays before your object reference. You can always confirm that by using the trace() function: Place a trace(“frame“) in the keyframe that contains your object. (If you have a document class, it will need to extend the MovieClip class to do this.) And place a trace(“code“) just above the problematic line of code and test. If you see “code” and then your error message, your code is executing before your frame plays. Fix your code so it doesn’t execute until the object exists. I cannot tell you how to do that because the solution will vary depending on your project.
Errors That Trigger an Error Message If you see “frame” and then “code”, you have confirmed your object exists before your code tries to reference it, so something else is causing the problem. This always boils down to Flash being confused because you have or had more than one object with that reference name. To solve this problem, move the on-stage object to its own layer, clear all keyframes in that layer except one, copy its reference from your ActionScript, and paste it into the Properties panel. Use Movie Explorer to confirm there is no ActionScript creating a duplicate reference and there is no other on-stage object with the same reference. Then test. There will be no 1009 referencing that object if you follow those steps and the ActionScript referencing the object executes no sooner than the keyframe containing the object. Now, add other needed keyframes and change the object properties as needed. Another way this error can occur is when trying to reference a loader’s content property before loading is complete: var loader:Loader = new Loader(); loader.load(new URLRequest(“test.swf“)); // make sure you have a test.swf in the correct directory trace(MovieClip(loader.content).totalFrames); // will trigger a 1009 error Because a loader’s content property is null until loading is complete, use an Event.COMPLETE listener and listener function before referencing the loader’s content. var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE,completeF); loader.load(new URLRequest(“test.swf“)); // make sure you have a test.swf in the correct directory function completeF(e:Event):void{ trace(MovieClip(loader.content).totalFrames); // no error } Also, a DisplayObject’s root and stage properties are null until the object is added to the display list. This error frequently occurs in a DisplayObject’s class file, where the object is created using the new constructor, the object’s class constructor executes, and a stage reference is made before the object is added to the display list. For example: var custom_mc:Custom_MC = new Custom_MC(); addChild(custom_mc); will trigger this error if Custom_MC looks something like: package { import flash.display.MovieClip; 577
578 Appendix A n Errors That Trigger an Error Message public class Custom_MC extends MovieClip { public function Custom_MC() { // stage is null at this point, triggering a 1009 error this.x=stage.stageWidth/2; } } } To test if the problem is whether an object has been added to the display list, you can use: trace(this.stage); added above the problematic line of code. The trace output will be null. For example: package { import flash.display.MovieClip; public class Custom_MC extends MovieClip { public function Custom_MC() { trace(this.stage); this.x=stage.stageWidth/2; } } } To remedy this problem, use the ence the stage: Event.ADDED_TO_STAGE listener before trying to refer- package { import flash.display.MovieClip; public class Custom_MC extends MovieClip { public function Custom_MC() { this.addEventListener(Event.ADDED_TO_STAGE,init); } private function init(e:Event):void{ this.x=stage.stageWidth/2; // stage is defined when this executes } } } 1013: The Private Attribute May Be Used Only on Class Property Definitions This is commonly caused by mismatched curly brackets {} in a class file in a line of code above the line that is mentioned in the error message. 1046: Type Was Not Found or Was Not a Compile-Time Constant: xxxx You forgot to save the xxx class file, you have a typo, or you need to import the needed class, xxxx.
Errors That Trigger an Error Message To remedy the later, open the Flash help files > Classes > xxxx. At the top you will see a package listed (for example, fl.controls) that you will use in the needed import statement. Add the following to your scope: import fl.controls.xxxx (And in this example, the needed class type is a component, so you’ll need that component in your FLA’s library, too.) 1061: Call to a Possibly Undefined Method xxxx through a Reference with Static Type flash.display:DisplayObject You are trying to reference a method defined on a MovieClip timeline using dot notation, but the Flash compiler doesn’t recognize that MovieClip as a MovieClip. To remedy this, explicitly cast the MovieClip. For example, you have a function/method which you are trying to reference using: xxxx() defined on your root timeline, root.xxxx(); To remedy, cast root as a MovieClip: MovieClip(root).xxxx(); 1067: Implicit Coercion of a Value of Type xxxx to an Unrelated Type yyyy You are trying to assign an object from class xxxx a value from class yyyy. The most common error of this type seen on the Adobe forums is when trying to assign text to a TextField and the code forgets to use the TextField’s text property: var tf:TextField = new TextField(); tf = “This is test text.”; // should be using tf.text = “This is test text.”; 1078: Label Must Be a Simple Identifier I get this error frequently because I’m speed typing, and instead of a semicolon, I have the Shift key pressed and add a colon at the end of a line of code. Check the line of code mentioned in the error message and look for a misplaced colon (typically the last character in the line). 1118: Implicit Coercion of a Value with Static Type xxxx to a Possibly Unrelated Type yyyy Often loaded content needs to be cast. For example, if you load a SWF and try to check a MovieClip property (for example, totalFrames), you will need to cast it as a MovieClip: var loader:Loader = new Loader(); 579
580 Appendix A n Errors That Trigger an Error Message loader.contentLoaderInfo.addEventListener(Event.COMPLETE,completeF); loader.load(new URLRequest(“someswf.swf“)); function completeF(e:Event):void{ trace(e.target.loader.content.totalFrames): // error expected trace(MovieClip(e.target.loader.content).totalFrames)); // no error expected } Or, if you are loading xml: var xml:XML; var loader:URLLoader = new URLLoader(); loader.addEventListener(Event.COMPLETE,completeF); loader.load(new URLRequest(“somexml.xml“)); function completeF(e:Event):void{ xml = e.target.data // error expected xml = XML(e.target.data); // no error expected } When you use getChildByName, the compiler only knows the object is a DisplayObject. If you want to use a property that is not inherited by DisplayObjects (for example, mouseEnabled), you need to cast the object: for (var i:int=0;i<4;i++) { var opBtn:Btn_operator = new Btn_operator(); // where the Btn_operator class // extends an InteractiveObject class opBtn.name=i.toString(); addChild(opBtn); } // then you can use: for (i=0;i<4;i++) { opBtn = Btn_operator(getChildByName(i.toString())); opBtn.mouseEnabled = false; } 1119: Access of Possibly Undefined Property xxx through a Reference with Static Type yyyy You’re trying to reference a property that doesn’t exist for that class type. There are three possiblities: n You have a typo and should correct your typing to eliminate the error. n You are trying to reference a property that does not exist for that object’s data type, and you should check the ActionScript help files to remedy. n You are trying to reference a property that does exist for that object’s data type, but the compiler is confused and is displaying a misleading error message. In that situation, explicitly cast the data type of the object.
Errors That Trigger an Error Message For example, if you have a timeline: DisplayObject (for example, dobj) added to the root root.dobj; // may trigger a 1119 error MovieClip(root).dobj; // will not trigger a 1119 error In fact, when you use root in ActionScript 3, you almost always will need to case it as a MovieClip unless you have a document class that extends the Sprite class. In that situation, you will need to cast root as a Sprite: MovieClip(root); // or Sprite(root): Also, if this.parent is a MovieClip: this.parent.ivar; // may trigger a 1119 error MovieClip(this.parent).ivar; // will not trigger a 1119 If you’re using one of your own class objects: var opBtnParent:Sprite = new Sprite(); addChild(opBtnParent); for (var i:int=0;i<4;i++) { var opBtn:Btn_operator = new Btn_operator(); opBtnParent.addChild(opBtn); } // then you can use: for (i=0;i<opBtnParent.numChildren;i++) { Btn_operator(opBtnParent.getChildAt(i)).mouseEnabled = false; } Or, you may be trying to reference an object created outside the class. In that situation, make sure your class is dynamic, for example: dynamic public class YourClass extends MovieClip{ You can now add properties to YourClass instances from outside YourClass. 1120: Access of Undefined Property Error xxxx Either you have a typo, you have forgotten that ActionScript is case sensitive, or you have defined the variable somewhere but your reference is out of scope of the defined variable. Typically, the latter occurs when you make a variable local to a function and then try to reference the variable outside that function. For example: function f():void{ var thisVar:String = “hi“; } trace(thisVar); 581
582 Appendix A n Errors That Trigger an Error Message will trigger an 1120 error because thisVar is local to f() (as a consequence of prefixing its definition with the keyword var inside a function body). To remedy, use the following (but you will need to call f() before thisVar has value “hi“): var thisVar:String; function f():void{ thisVar = “hi“; } trace(thisVar); Check the “Function Scope” section in Chapter 2 for more information. 1120: Access of Undefined Property Mouse Import the needed class flash.ui.Mouse: import flash.ui.Mouse; 1151: A Conflict Exists with Definition xxxx in Namespace Internal You have more than one of the following statements in the same scope: var xxxx:SomeClass; // and/or var xxxx:SomeClass = new SomeClass(); To remedy, change all but the first to: xxxx = new SomeClass(); 1152: A Conflict Exists with Inherited Definition xxxx in Namespace Public You’re trying to use an ActionScript keyword as a variable. To remedy this, change that variable’s name to a non-keyword. 1178: Attempted Access of Inaccessible Property xxxx through a Reference with Static Type YYYY Variable xxxx typed private in class YYYY when should be protected, internal or public. 1180: Call to a Possibly Undefined Method addFrameScript You have code (or even an empty space) on a timeline, and your document class is extending the Sprite class. To remedy this, extend MovieClip or remove code from all timelines. To find timeline code, use Movie Explorer and toggle only the Show ActionScript button. 1180: Call to a Possibly Undefined Method xxxx If you think you have defined that method, check for a typo. In other words, check your method’s spelling and use copy and paste to eliminate typo issues.
Errors That Trigger an Error Message Or, you have a path problem. To remedy this, check your path and check the relevant (MovieClip or class) scope section in Chapter 2 to resolve path problems. Or, if xxxx is a class name, you failed to define/save that class in the correct directory. To remedy this, save the needed class file and make sure the method xxxx is defined in your class. Or, you are trying to use a method defined in a class that has not been imported. To remedy this, import the needed class. Or, you are trying to use a method xxxx that is not inherited by your class. For example, trying to use addEventListener in a custom class that does not extend a class with the addEventListener method will trigger a 1180 error. To remedy this, extend a class that has this method. Or, you nested a named function. To remedy, un-nest it. In ActionScript, you can use named and anonymous functions. A named function has the form: function f1():void{ } An anonymous function has the form: var f1:Function = function():void{ } In the following code, both f1(). f1() and f2() are named functions, and f2() is nested in function f1():void{ trace(“f1“); function f2():void{ trace(“f2“); } } If you use this code, at no time will f2() be defined outside of f1(). If you try to call f2() from outside of f1(), no matter how many times you call f1(), you will generate an 1180 error: Call to a Possibly Undefined Method. To remedy this, un-nest: function f1():void{ } function f2():void{ } 583
584 Appendix A n Errors That Trigger an Error Message Alternatively, nest an anonymous function in a named function: var f2:Function; function f1():void { f2 = function():void{ }; } Or, nest two anonymous functions: var f2:Function; var f1:Function = function():void { f2 = function():void{ }; } Note that f2 is declared outside of outside the f1 function body. f1. That is required if you want to call f2 from If you tried: function f1():void { var f2:Function = function():void{ trace(“f2“); }; f2(); // this will work } But, if you try: function f1():void { var f2:Function = function():void{ trace(“f2“); }; } f1(); f2(); You will trigger an 1180: Call to a Possibly Undefined Method f2 error. You can expect the same error if you nest a named function in an anonymous function. You can nest name and anonymous functions if you only call the nested function from within the nesting function body. For example, the following two snippets will not trigger an error. Snippet 1: function f1():void{ trace(1);
Errors That Trigger an Error Message f2(); function f2():void{ trace(2); } f2(); } f1(); Snippet 2: var f1:Function=function():void{ trace(1); f2(); function f2():void{ trace(2); } f2(); } f1(); Nevertheless, although you can nest a named function in this situation without triggering an error, you should not. There is no benefit to nesting a named function, and in addition to triggering 1180 errors, it makes your code less readable. 1203: No Default Constructor Found in Base Class xxx Failure to call the superclass (or no constructor). To remedy this, either create a constructor or use super() in your already existing constructor. SecurityError: Error #2000: No Active Security Context The file you are trying to load does not exist. You probably have a typo or a path problem. If your file loads when tested locally but triggers that error when test online, look for case mismatches. For example, trying to load file.SWF will load file.swf locally but not online. TypeError: Error #2007: Parameter Text Must Be Non-Null You are trying to assign undefined text to a the following will trigger this error: var tf:TextField = new TextField(); var a:Array = [1,2]; var s:String; tf.text = s; // and tf.text = a[2]; // will both trigger this error TextField’s text property. For example, 585
586 Appendix A n Errors That Trigger an Error Message Error #2044: Unhandled IOErrorEvent:. text=Error #2035: URL Not Found You have an incorrect path and/or file name. First, double-check your spelling and your paths. Then make sure your load target’s path is relative to the location of your SWF’s embedding HTML file’s location, if you are testing by opening the HTML in a browser. If you are testing in the Flash IDE or testing by opening the SWF directly, the path should be relative to the SWF. Error #2047: Security Sandbox Error Error #2048: Security Sandbox Error Error #2049: Security Sandbox Error You are probably trying to test a SWF file on your local computer (which is in your local security sandbox) and that SWF is trying to connect to the Internet across that security sandbox. To remedy, either upload your test files to a server on the Internet or adjust your security settings at www.macromedia.com/support/documentation/en/ flashplayer/help/settings_manager04.html. 2136: The SWF file file:///somepath/someswf.swf Contains Invalid Data You are trying to use the “new” constructor with a document class. To remedy this, either remove that class from your document class or remove the “new” constructor applied to that class. 5000: The Class ‘xxx’ Must Subclass ‘yyy’ Since It Is Linked to a Library Symbol of That Type Can occur because a previous error stoned the compiler, triggering errors that do not exist. But if it’s the first (or only) error listed, your ‘xxx’ class must usually extend a MovieClip or SimpleButton. Right-click your library symbol, click Properties, and check the base class listed there. 5008: The Name of Definition ‘Xxxx’ Does Not Reflect the Location of This File. Please Change the Definition’s Name inside This File, or Rename the File. The file name and the class name must match. For example: package { public class Xxxx { public function Xxxx(){ } } } must be saved as Xxxx.as (and case matters).
Appendix B Errors That Do Not Trigger Error Messages Use this appendix to find and correct coding errors that do not trigger a compiler error message. Code That Doesn’t Work This is a common lament. Adobe forum posters frequently state that they see no error message, but their code doesn’t work. Finding the problem and then the solution always involves the same steps: Use the trace() function and then use it again and again and again until you pinpoint the problem. You can always pinpoint the problem using the trace() function. An example of code that doesn’t work would be code for a button that, when clicked, should load a SWF, but when you click the button, the SWF doesn’t load. That would typically be posted as, Why doesn’t my SWF load? But anyone who has been answering forum posts for more than a few months knows it is not a good idea to assume the poster is correct. That SWF may be loading and the poster doesn’t realize it. The only thing that can be safely assumed is that the poster clicked something and failed to see what he expected to see (the loaded SWF). That might mean the SWF didn’t load, but it also might mean the button that is supposed to start the SWF loading wasn’t clicked, or the button was clicked and the SWF did load but isn’t added to the display list. Or, it loaded and is added to the display list, but it isn’t visible because something is covering it. Or, the SWF is positioned off-stage, or its alpha is 0, or its 587
588 Appendix B n Errors That Do Not Trigger Error Messages visible property is false. Or, maybe the SWF was loaded and added to the display list and would’ve been visible, but something occurred before the display was rendered to make the loaded SWF not visible. There are almost always a lot of reasons why code can seem not to work. To find the problem, you should start by pinpointing exactly where your code fails. In other words, use the trace() function. So, let’s debug this code, which should load test.swf and display it when btn is clicked: btn.addEventListener(MouseEvent.CLICK,btnF); function btnF(e:MouseEvent):void { var loader:Loader = new Loader(); loader.load(new URLRequest(“test.swf“)); addChild(loader); } If you don’t see what you expect, first confirm that what you expect to see is reasonable by clicking test.swf or its embedding HTML to confirm that you can see something on-stage and thereby judge whether test.swf is displayed. Or, use the trace() function in test.swf to judge whether it is loaded. Having done that, you should make sure that code executes. Depending on where that code is located, it might not execute. To check whether that code is executing, you could use: trace(“OK“); btn.addEventListener(MouseEvent.CLICK,btnF); function btnF(e:MouseEvent):void { var loader:Loader = new Loader(); loader.load(new URLRequest(“test.swf“)); } When you test that code, you should see OK in the output panel (or that code is not executing or you disabled trace output). If that code is not executing (in other words, you do not see OK in the output panel), you need to fix that: 1. If that code is timeline code, you can conclude that it is attached to a frame that hasn’t played at the time you clicked btn. 2. If it’s in a class, that class has not been instantiated when btn is clicked. 3. And/or that listener is added within a function that needs to be called before btn is clicked.
Code That Doesn’t Work Assuming you see OK in the output panel, the next step would be to confirm your btn listener function (btnF) is being called: btn.addEventListener(MouseEvent.CLICK,btnF); function btnF(e:MouseEvent):void { trace(e); var loader:Loader = new Loader(); loader.load(new URLRequest(“test.swf“)); addChild(loader); } After clicking btn, you should see a MouseEvent.CLICK event in your output panel ([MouseEvent type=“click” etc]). If you do not, btn is not being clicked. You might think you’re clicking btn, but Flash does not. There are quite a few ways that can happen. Because there is no error message, you know Flash is finding something with reference btn. So, btn exists. It is just not the object you’re clicking. You should check for all btn objects by using Movie Explorer. If you find more than one, that may be the problem. Create a test FLA and confirm that multiple btn objects are the problem by removing all but the one closest to Frame 1 of the parent timeline and retesting your code. If that works, re-add the other keyframes that contain btn (without dragging anything from the library), and the error shouldn’t return. If you do see [MouseEvent type=“click” etc] in the output panel and there are no error messages, you know test.swf exists in a valid path (otherwise, Flash would generate an Error #2044: Unhandled IOErrorEvent:. text=Error #2035: URL Not Found message). But, you can add an Event.COMPLETE listener with a trace() function to confirm that test.swf is loading. If none of the above reveals the problem, then you know test.swf is loading. To find out why it’s not being displayed, you will need to execute code some milliseconds after loading is complete to see whether loader is still on-stage and visible or whether some other code is moving, removing, or otherwise changing loader so its content is no longer visible. The following code should do that, allowing you to check whether loader is being used to load something else or its alpha or x or visible or some other property is causing the problem. btn.addEventListener(MouseEvent.CLICK,btnF); var loader:Loader = new Loader(); var t:Timer = new Timer(100,1); 589
590 Appendix B n Errors That Do Not Trigger Error Messages t.addEventListener(TimerEvent.TIMER,traceF); function btnF(e:MouseEvent):void { loader.load(new URLRequest(“test.swf“)); addChild(loader); t.start() } function traceF(e:TimerEvent):void{ // The url property should show test.swf confirming loader is not // being used to load something other than test.swf. // The numChildren property should be one greater than loader’s // childIndex or something is being positioned over loader. trace(loader.content.loaderInfo.url,loader.parent.numChildren,loader.parent.get ChildIndex(loader)) var xml:XML = describeType(loader); var xmlList:XMLList = xml.accessor; for(var i:int=0;i<xmlList.length();i++){ if(xmlList[i].@access.indexOf("read")>-1){ // This will reveal most of the properties of loader // and their values (like alpha, x, y, visible, etc). trace(xmlList[i].@name,loader[xmlList[i].@name]) } } } You will need to check those properties and values carefully. While it would obviously be a problem if a property like visible is false, there are less obvious ways a property change can cause a problem. For example, if you are loading a SWF that contains only non-embedded classic text, and you rotate your loader or its content, you won’t see anything on-stage from the loaded SWF until you embed the text’s font. Errors Caused by Asynchronous Code Execution Most code in Flash executes in an orderly, easy-to-follow way. If you have: f1(); // where functions f1() and f2() are defined in this scope f2(); you can be sure the code in f1() will execute before the code in f2(). That is an example of synchronous code execution: If statement1 precedes statement2, statement1 executes before statement2. Most ActionScript executes synchronously.
gotoAndPlay and gotoAndStop (to a Frame Not Yet Loaded) But some code executes asynchronously. In particular, file loading is asynchronous, and load-complete listener functions execute asynchronously. For example: var urlLoader:URLLoader = new URLLoader(); urlLoader.load(new URLRequest(“test.txt“)); // test.txt should be in the same file with your flash files trace(urlLoader.data); // undefined because loading is not complete and var dataS:String; var urlLoader:URLLoader = new URLLoader(); urlLoader.addEventListener(Event.COMPLETE,completeF); urlLoader.load(new URLRequest(“test.txt“)); function completeF(e:Event):void{ dataS = urlLoader.data; } trace(urlLoader.data); // undefined because this executes before completeF() executes trace(dataS); // null because dataS is typed but no value is assigned until completeF() executes To remedy, you should use listener functions to trigger code that depends on loaded assets: var dataS:String; var urlLoader:URLLoader = new URLLoader(); urlLoader.addEventListener(Event.COMPLETE,completeF); urlLoader.load(new URLRequest(“test.txt“)); function completeF(e:Event):void{ dataS = urlLoader.data; dependentCodeF(); } function dependentCodeF():void{ trace(dataS); // etc } gotoAndPlay and gotoAndStop (to a Frame Not Yet Loaded) If you use either of these and find that Flash isn’t going to the correct frame, after checking for typos you should ensure that the target frame is loaded. This is more likely to occur when you’re testing online, but I’ve seen the same issue when testing locally, too. 591
592 Appendix B n Errors That Do Not Trigger Error Messages To remedy this, don’t execute the goto until the needed frame is loaded. It’s easiest to use a complete listener to ensure that all frames are loaded if you encounter this problem: goto is applied to the SWF that contains your code this.loaderInfo.addEventListener(Event.COMPLETE,completeF); function completeF(e:Event):void{ this.gotoAndStop(this.totalFrames); } Example 1: goto is applied to a loaded swf var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE,completeF); loader.load(new URLRequest(“test.swf“)); function completeF(e:Event):void{ var mc:MovieClip = MovieClip(loader.content); // the loader’s content property, after loading is complete, is the loaded swf’s main timeline (which is a MovieClip). Unfortunately, the Flash compiler (in strict mode) often loses track of what object is of what class type and triggers a mc.gotoAndStop(mc.totalFrames); } Example 2: The Lost Object References There are many ways this occurs, but most commonly it occurs in for loops and while loops. One reference is used repeatedly, so with each loop iteration the current object reference overwrites the previous object reference. At the end of the for loop, that reference refers to only the last created object. Here’s the typical problem (already mentioned in Chapter 1): for (var io:int = 0; io < 4; io++) { var opBtn:Btn_operator = new Btn_operator(); opBtn.addEventListener(MouseEvent.CLICK, pressOperator); } And then later in the code: opBtn.mouseEnabled = false; // only one opBtn instance will be disabled Among the many ways to resolve this issue, you can: 1. Assign a name property to each object and use the to the object’s parent: getChildByName() for (var i:int=0;i<4;i++) { var opBtn:Btn_operator = new Btn_operator(); method applied
Lost Object References opBtn.name=i.toString(); addChild(opBtn); } // then you can use: for (i=0;i<4;i++) { var btn = getChildByName(i.toString()); // if you use an opBtn reference, use Appendix A to debug the Flash error btn.mouseEnabled = false; } 2. Push each object into an array and later loop through the array: var opBtnA:Array = []; for (var i:int=0;i<4;i++) { var opBtn:Btn_operator = new Btn_operator(); opBtnA.push(opBtn); addChild(opBtn); } // then you can use: for (i=0;i<opBtnA.length;i++) { opBtnA[i].mouseEnabled = false; } 3. Add each object to a parent that contains nothing but those objects and loop through the parent’s children: var opBtnParent:Sprite = new Sprite(); addChild(opBtnParent); for (var i:int=0;i<4;i++) { var opBtn:Btn_operator = new Btn_operator(); opBtnParent.addChild(opBtn); } // then you can use: for (i=0;i<opBtnParent.numChildren;i++) { Btn_operator(opBtnParent.getChildAt(i)).mouseEnabled = false; } 4. Use a different object reference in each loop: for (var i:int=0;i<4;i++) { this["opBtn"+i] = new Btn_operator(); } // then you can use: for (i=0;i<4;i++) { this["opBtn"+i].mouseEnabled = false; } 593
594 Appendix B n Errors That Do Not Trigger Error Messages Repeatedly Executed Code This only happens when code is added to a timeline frame and that frame repeatedly plays. For example, if the following code is attached to a frame that repeatedly executes, you will find f() being executed more and more frequently. var t:Timer = new Timer(1000,0); t.addEventListener(TimerEvent.TIMER,f); t.start(); function f(e:TimerEvent):void{ trace(getTimer()); } To remedy, use a Boolean so that code executes only the first time the frame plays: var alreadyExecuted:Boolean; if (! alreadyExecuted) { alreadyExecuted=true; var t:Timer=new Timer(1000,0); t.addEventListener(TimerEvent.TIMER,f); t.start(); function f(e:TimerEvent):void { trace(getTimer()); } } The above code is also an example of a runaway loop defined by an accelerating loop. If you use setInterval(), you can prevent it from triggering runaway loops by clearing the interval prior to each setting: var testI:int; clearInterval(testI); testI = setInterval(testF,1000); function testF(){ trace(getTimer()); } or enclose in a Boolean. Or, even better, do both: var alreadyExecuted:Boolean; if (! alreadyExecuted) { var testI:int; clearInterval(testI); testI = setInterval(testF,1000); function testF(){ trace(getTimer()); } }
Problems Related to Class File Use Problems Related to Class File Use 1. To see the results of a changed class file, you must save your changes and then test or publish a new SWF. 2. If, in your default directory, you have a class file C1.as: package { public class C1 { public function C1() { trace(“C1“); } } } and in a subdirectory Sub of your default directory, you have a different class file C1.as: package Sub{ public class C1 { public function C1() { trace(“C.C1“); } } } then in any other class instantiating a C1 instance, even if Sub.C1 is imported into that class and no matter the location of the other class, the C1 class from the default directory will be used, not the C1 class from the Sub directory. 595
This page intentionally left blank
INDEX #2000 No Active Security Context error message, 585 #2007 Parameter Text Must Be Non-Null error message, 585 #2035 URL Not Found error message, 586 #2044 Unhandled IOErrorEvent. text=Error #2035 URL Not Found error message, 586 #2047 Security Sandbox Error error message, 586 #2048 Security Sandbox Error error message, 586 #2049 Security Sandbox Error error message, 586 % (modulo operator), 67–68, 214 +1 button (Google+), 509 3D games (Flare3D) 3D particle effects, 421–435 astro character, 393–401 cameras, 393–401 cost, 384 Data class, 392 downloading, 384 energy 3D model, 393–401 example directory, 421–435 fans, 393–401 files, 384–386 first-person view, 421–435 GameOverView class, 391–392 GameView class, 389–391 initializing world, 387–392 IntroView class, 388–389 libraries, 384–386 Main class, 387–388 mines, 393–401 objects animating, 393–401 collision detection, 401–410 shadows, 411–420 opening, 386 overview, 383–384 ParticleEmitter3D class, 421–435 preloader display, 393–401 score, 411–420 sound, 411–420 terminology, 384 3D particle effects (Flare3D), 421–435 1009 Cannot Access a Property or Method of a Null Object Reference error message, 575–578 1013 The Private Attribute May Be Used Only on Class Property Definitions error message, 578 1046 Type Was Not Found or Was Not a Compile-Time Constant error message, 578–579 1061 Call to a Possibly Undefined Method xxxx through a Reference with Static Type flash.display, 579 1067 Implicit Coercion of a Value of Type xxxx to an Unrelated Type yyyy error message, 579 1078 Label Must Be a Simple Identifier error message, 579 1118 Implicit Coercion of a Value with Static Type xxxx to a Possibly Unrelated Type yyyy error message, 579–580 1119 Access of Possibly Undefined Property xxxx through a Reference with Static Type yyyy error message, 580–581 1120 Access of Undefined Property Error xxxx error message, 581–582 1120 Access of Undefined Property Mouse error message, 582 1151 A Conflict Exists with Definition xxxx in Namespace Internal error message, 582 1152 A Conflict Exists with Definition xxxx in Namespace Public error message, 582 1178 Attempted Access of Inaccessible Property xxxx through a Reference with Static Type yyyy, 582 1180 Call to a Possibly Undefined Method addFrameScript error message, 582 597
598 Index 1180 Call to a Possibly Undefined Method xxxx error message, 582–585 1203 No Default Constructor Found in Base Class xxx error message, 585 2000 No Active Security Context error message, 585 2007 Parameter Text Must Be Non-Null error message, 585 2035 URL Not Found error message, 586 2044 Unhandled IOErrorEvent. text=Error #2035 URL Not Found error message, 586 2047 Security Sandbox Error error message, 586 2048 Security Sandbox Error error message, 586 2049 Security Sandbox Error error message, 586 2136 The SWF file file…someswf. swf Contains Invalid Data error message, 586 5000 The Class xxx Must Subclass yyy Since It Is Linked to a Library Symbol of That Type error message, 586 5008 The Name of Definition Xxxx Does Not Reflect the Location of This File. Please Change the Definition Name inside This File, or Rename the File. error message, 586 10,000 MovieClips test Starling, 242–247 A A Conflict Exists with Definition xxxx in Namespace Internal error message, 582 A Conflict Exists with Definition xxxx in Namespace Public error message, 582 Access of Possibly Undefined Property xxxx through a Reference with Static Type yyyy error message, 580–581 Access of Undefined Property Error xxxx error message, 581–582 Access of Undefined Property Mouse error message, 582 ActionScript ActionScript 3.0 API Language Reference, 81–84 Facebook ActionScript API, 478–482 Google+ ActionScript 3.0 library, 533–552 Google+ API, 513 JavaScript communication, 56–57, 445–451, 478 Tweetr ActionScript library, 494–508 ActionScript 3.0 API Language Reference, 81–84 ActionScript API (Facebook), 478–482 ADB (Android Debug Bridge), 371–372 addChild() method, 87 adjacent angles, 62 Adobe Monocle, 194 Adobe Scout, 194–195 ADT (Adobe AIR Developer Tool), 372–374 AIR Debug Launcher, testing Android games, 368 Air for Android Deployment tab, 377–380 General tab, 374–376 Icons tab, 380 Languages tab, 381–382 Permissions tab, 380–381 Air for iOS Deployment tab, 327–330 General tab, 323–327 Icons tab, 330 Languages tab, 330–331 Android Debug Bridge (ADB), 371–372 Android games Android Debug Bridge (ADB), 371–372 Controller class, 349–359 Data class, 361–362 distributing, 382 GameOverView class, 359–360 GameView class, 347–349 IntroView class, 342–346 Light_Pool class, 366–367 Main class, 339–342 overview, 337–339 publishing (Air for Android) Deployment tab, 377–380 General tab, 374–376 Icons tab, 380 Languages tab, 381–382 Permissions tab, 380–381 Replay_Btn class, 363 Slider class, 363–365 Switch class, 362 Switch_Pool class, 366 testing ADB (Android Debug Bridge), 371–372 ADT (Adobe AIR Developer Tool), 372–374 AIR Debug Launcher, 368 emulators, 368–371 overview, 367 TF class, 362–363 TF_Pool class, 367 virtual device emulator, 194 angles adjacent angles, 62 angles of reflection, 62–64 Pythagorean Theorem, 64–65 straight angles, 61 sum of triangle’s angles, 61 vertical angles, 62 animating objects (Flare3D), 393–401 APIs Flash, 81–84 Google+. See Google+ Stage3D (Flare3D) 3D particle effects, 421–435 animating objects, 393–401 astro character, 393–401 cameras, 393–401 collision detection, 401–410 cost, 384 Data class, 392 downloading, 384 energy 3D model, 393–401 example directory, 421–435 fans, 393–401 files, 384–386
Index first-person view, 421–435 GameOverView class, 391–392 GameView class, 389–391 initializing world, 387–392 IntroView class, 388–389 libraries, 384–386 Main class, 387–388 mines, 393–401 opening, 386 overview, 383–384 ParticleEmitter3D class, 421–435 preloader display, 393–401 score, 411–420 shadows, 411–420 sound, 411–420 terminology, 384 appID (Facebook JavaScript API), 451–452 applications (Google+ API sample Flash applications), 518–533 arithmetic (CPU/GPU usage), 212–218 arrays associative arrays, 50 copying, 49 CPU/GPU usage, 256–258 creating, 47–50 Dictionary, 55 key-press combinations, 110–114 notation, 50–51 shuffling, 49–50 strings, 48–49 vectors, 78–79 assigning undeclared properties, 45–46 associative arrays, 50 astro character (Flare3D), 393–401 asynchronous errors, 6, 590–591 Attempted Access of Inaccessible Property xxxx through a Reference with Static Type yyyy, 582 authentication (Twitter requests) not required, 486–491 required, 492–508 authorizing requests (Google+ API), 517–533 auto-formatting, 16 B badges (Google+), 509–510 BitmapData class, 51–54 bitmaps BitmapData class, 51–54 CPU/GPU usage cacheAsBitmap property, 219–220 cacheAsBitmapMatrix property, 219–220 partial blitting, 228–231 stage blitting, 220–228 bitwise operators (CPU/GPU usage), 212–218 blitting CPU/GPU usage partial blitting, 228–231 stage blitting, 220–228 Starling comparison, 236–256 boundary violations, 110–114, 149–172 brackets (errors), 6–7 break statements, 60 browsers, cookies, 76 buttons Google+ +1, 509 code, 510–513 Share, 510 Twitter Follow, 485 Hashtag, 485 Mention, 485–486 Share a Link, 484 C cacheAsBitmap property (CPU/ GPU usage), 219–220 cacheAsBitmapMatrix property (CPU/GPU usage), 219–220 calculating distance (objects), 64–65 Call to a Possibly Undefined Method addFrameScript error message, 582 Call to a Possibly Undefined Method xxxx error message, 582–585 Call to a Possibly Undefined Method xxxx through a Reference with Static Type flash.display, 579 callback functions, managing memory, 205–206 cameras (Flare3D), 393–401 Cannot Access a Property or Method of a Null Object Reference error message, 575–578 certificates (iOS games) development certificates (publishing), 312–323 distribution certificates, 313, 331–335 changeRegPt() function, 75–76 characters (Flare3D astro character), 393–401 chunks (loops), 58–60 class coding, 17–18 classes BitmapData, 51–54 class coding, 17–18 CollisionDetection, 52 CombatView creating, 125–135 iOS games, 274–279 CombatView_Pool (iOS games), 279–280 constructors, 30, 36–37 Controller Android games, 349–359 iOS games, 260, 280–296 Controller_Pool (iOS games), 297 creating, 27–31 Data Android games, 361–362 Flare3D, 392 iOS games, 305–308 peer-to-peer multiplayer games, 572–574 directories, 29, 31 dynamic, 45–46 encapsulation, 95–105 EndView, 95. See also GameOverView class EnemyTank, 96–105. See also tanks creating, 86, 125–135 iOS games, 301–302 599
600 Index classes (Continued) EnemyTank_Pool (iOS games), 302 events, dispatchers, 38–39, 56–58 extending, 30 ExternalInterface, 56–57, 445–451, 478 files directories, 96–97 pseudo classes, 97 Flash API, 82–84 GameOverView, 95. See also end view Android games, 359–360 Flare3D, 391–392 iOS games, 302–304 GameOverView_Pool (iOS games), 304–305 GameView class, 95–105 Android games, 347–349 Flare3D, 389–391 importing, 29, 31 IntroView class, 95–105, 172–192 Android games, 342–346 Flare3D, 388–389 iOS games, 262–271 IntroView_Pool (iOS games), 271–272 KBcontrols, 96–105 Keyboard, 88–89 KeyboardEvent, 89–91 KeyboardType, 88–89 Light_Pool (Android games), 366–367 Main Android games, 339–342 creating, 81–82 Flare3D, 387–388 iOS games, 260–262 organizing, 95–105 peer-to-peer multiplayer games, 554–569 Math (CPU/GPU usage), 215–218 methods. See also functions addChild(), 87 FB (Facebook JavaScript API), 453 gotoAndPlay, 591–592 gotoAndStop, 591–592 hitTestObject(), 92–93 hitTestPoint(), 92–93 internal, 31–33, 42–43 private, 31–33, 42–43 protected, 31–33, 42–43 public, 31–33, 37, 42–43 static, 42–43 MovieClip, extending, 85–86 names, 30 NetGroup (peer-to-peer multiplayer games), 569–572 P2P (peer-to-peer multiplayer games), 569–572 packages, 29 ParticleEmitter3D, 421–435 PlayerTank class, 96–105. See also tanks creating, 85–86 iOS games, 299–300 PlayerTank_Pool (iOS games), 300–301 properties getters, 34–39 internal, 31–33 private, 31–33 protected, 31–33 public, 31–33 setters, 34–39 static, 39–41 Replay_Btn (Android games), 363 saving (SWF), 87 scope, 23–24 singletons, 43–45 Slider Android games, 363–365 iOS games, 272–274 Stage, 90 Switch (Android games), 362 Switch_Pool (Android games), 366 Tank (iOS games), 298–299 TF (Android games), 362–363 TF_Pool (Android games), 367 Timer (iOS games), 297 TimerExt (iOS games), 297 troubleshooting files, 595 Tween, 76–77 TweenLite, 76–77 Vector, 78–79 clearing memory (garbage collection), 57 listeners, 57–58 overview, 57 preparing objects, 135–149 code asynchronous errors, 6, 590–591 class coding, 17–18 conditional compiling, 54–55 Config Constants, 54–55 debugging errors that do not trigger error messages, 587–590 formatting, 16 Google+ buttons, 510–513 timeline coding, 17–18 troubleshooting repeatedly executed code, 594 collision detection CollisionDetection class, 52 Flare3D, 401–410 iOS games, 260 moving objects, 149–172 objects, 51–54, 401–410 pixels, 51–54 CollisionDetection class, 52 color, 51–54 CombatView class creating, 125–135 iOS games, 274–279 CombatView_Pool class (iOS games), 279–280 communication, ActionScript/ JavaScript, 56–57, 445–451, 478 compile-time errors, 3–5 conditional compiling, 54–55 Config Constants, 54–55 configuring (Config Constants), 54–55 constants (Config Constants), 54–55 constructors (classes), 30, 36–37 Controller class Android games, 349–359 iOS games, 260, 280–296 Controller_Pool class (iOS games), 297 controllers. See Controller class controlling parameters (users), 172–192
Index cookies (browsers), 76 copying arrays, 49 cost (Flare3D), 384 CPU/GPU usage. See also performance arrays, 256–258 bitmaps cacheAsBitmap property, 219–220 cacheAsBitmapMatrix property, 219–220 partial blitting, 228–231 stage blitting, 220–228 listeners, 256 loops, 231–233 math arithmetic, 212–218 bitwise operators, 212–218 division, 212–213 Math class, 215–218 modulo operator, 214 multiplication, 213–214 memory relationship, 195–197 mouse, 233–235 remote listeners, 235 Stage3D overview, 235–236 Starling 10,000 MovieClips test, 242–247 Starling Mudbubble Boy test, 250–255 Starling Patch the Scarecrow test, 247–250 Starling/blitting comparison, 236–256 variables, 256 vectors, 256–258 creating arrays, 47–50 classes, 27–31 CombatView class, 125–135 custom cursors, 114–118 EnemyTank class, 86, 125–135 game-over views, 149–172 games, 86 Main class, 81–82 objects, 125–135 PlayerTank class, 85–86 registration points, 85 rotating objects, 114–118 shooting functionality, 114–118 sound, 125–135, 149–172 statistics displays, 149–172 views, 125–135 cursors, creating, 114–118 custom profiles, debugging, 8–9 D dashes (errors), 7 data, organizing, 172–192 Data class Android games, 361–362 Flare3D, 392 iOS games, 305–308 peer-to-peer multiplayer games, 572–574 debugging. See also errors; testing compile-time errors, 3–5 custom profiles, 8–9 errors that do not trigger error messages, 587–590 formatting, 16 line numbers, 3 online, 2 permit debugging, 2–8 run-time errors, 3–8 scope, 17 templates, 18 textfields, 1–2 tools, 1–2 trace() function, 1–2, 11–14 delay (keyboard repeat delay), 104–110 Deployment tab Air for Android, 377–380 Air for iOS, 327–330 detecting collisions. See collision detection development certificates (publishing iOS games), 312–323 development provisioning profiles (publishing iOS games), 312–323 development/target platform comparison, 194–195 device emulator, 194 Dialogs (Facebook JavaScript API), 453–466 Dictionary, 55 directories classes, 29, 31, 96–97 Flare3D example directory, 421–435 dispatchers (events), 38–39, 56–58 dispatchEvent (managing memory), 205–206 displays. See views/displays distance, calculating, 64–65 distributing Android games, 382 iOS games, 313, 331–335 distribution certificates (distributing iOS games), 313, 331–335 distribution provisioning profiles (publishing iOS games), 313, 331–335 division (CPU/GPU usage), 212–213 do loops, 58–60 downloading Flare3D, 384 drawing tanks, 85 dynamic classes, 45–46 E effects (Flare3D 3D particle effects), 421–435 emulators testing Android games, 368–371 virtual device emulator, 194 encapsulation (classes), 95–105 EndView class, 95. See also GameOverView class EnemyTank class, 96–105. See also tanks creating, 86, 125–135 iOS games, 301–302 EnemyTank_Pool class (iOS games), 302 energy 3D model (Flare3D), 393–401 enterFrame loops, 60–61, 124–125 Error #2035 URL Not Found error message, 586 Error #2044 Unhandled IOErrorEvent. text=Error #2035 URL Not Found error message, 586 Error #2047 Security Sandbox Error error message, 586 Error #2048 Security Sandbox Error error message, 586 Error #2049 Security Sandbox Error error message, 586 601
602 Index errors. See also debugging; testing asynchronous, 6, 590–591 brackets, 6–7 compile-time errors, 3–5 dashes, 7 number of, 16 run-time errors, 3–8 scope, 17 SWF names, 7–8 that do not trigger error messages, 11–14 asynchronous code execution, 590–591 debugging code, 587–590 troubleshooting class files, 595 troubleshooting lost object references, 592–593 troubleshooting repeatedly executed code, 594 troubleshooting target frames, 591–592 that trigger error messages, 10 1009 Cannot Access a Property or Method of a Null Object Reference, 575–578 1013 The Private Attribute May Be Used Only on Class Property Definitions, 578 1046 Type Was Not Found or Was Not a Compile-Time Constant, 578–579 1061 Call to a Possibly Undefined Method xxxx through a Reference with Static Type flash.display, 579 1067 Implicit Coercion of a Value of Type xxxx to an Unrelated Type yyyy, 579 1078 Label Must Be a Simple Identifier, 579 1118 Implicit Coercion of a Value with Static Type xxxx to a Possibly Unrelated Type yyyy, 579–580 1119 Access of Possibly Undefined Property xxxx through a Reference with Static Type yyyy, 580–581 1120 Access of Undefined Property Error xxxx, 581–582 1120 Access of Undefined Property Mouse, 582 1151 A Conflict Exists with Definition xxxx in Namespace Internal, 582 1152 A Conflict Exists with Definition xxxx in Namespace Public, 582 1178 Attempted Access of Inaccessible Property xxxx through a Reference with Static Type yyyy, 582 1180 Call to a Possibly Undefined Method addFrameScript, 582 1180 Call to a Possibly Undefined Method xxxx, 582–585 1203 No Default Constructor Found in Base Class xxx, 585 2136 The SWF file file… someswf.swf Contains Invalid Data, 586 5000 The Class xxx Must Subclass yyy Since It Is Linked to a Library Symbol of That Type, 586 5008 The Name of Definition Xxxx Does Not Reflect the Location of This File. Please Change the Definition Name inside This File, or Rename the File., 586 Error #2044 Unhandled IOErrorEvent. text=Error #2035 URL Not Found, 586 Error #2047 Security Sandbox Error, 586 Error #2048 Security Sandbox Error, 586 Error #2049 Security Sandbox Error, 586 SecurityError Error #2000 No Active Security Context, 585 TypeError Error #2007 Parameter Text Must Be Non-Null, 585 events dispatchers, 38–39, 56–58 listeners CPU/GPU usage, 235, 256 dispatchers, 38–39, 57–58 keyboards, 89–91, 104–105 managing memory, 211 remote, 235 mouse, 74 rollout (mouse movement), 118–124 touch events (iOS games), 259 example directory (Flare3D), 421–435 experimenting (testing), 14–16 extending classes, 30 MovieClip class, 85–86 ExternalInterface class, 56–57, 445–451, 478 F Facebook ActionScript API, 478–482 JavaScript API ActionScript communication, 446–451 appID, 451–452 Dialogs, 453–466 FB methods, 453 JavaScript SDK, 452 overview, 444–446 permissions, 453 social graph, 466–477 registering games, 438–444 fans (Flare3D), 393–401 FB methods (Facebook JavaScript API), 453 files class directories, 96–97 Flare3D, 384–386 pseudo classes, 97 troubleshooting classes, 595 filters (managing memory), 206 finding objects (stages), 87–88 first-person view (Flare3D), 421–435 Fisher-Yates shuffle, 49–50 Flare3D 3D particle effects, 421–435 astro character, 393–401
Index cameras, 393–401 cost, 384 Data class, 392 downloading, 384 energy 3D model, 393–401 example directory, 421–435 fans, 393–401 files, 384–386 first-person view, 421–435 GameOverView class, 391–392 GameView class, 389–391 initializing world, 387–392 IntroView class, 388–389 libraries, 384–386 Main class, 387–388 mines, 393–401 objects animating, 393–401 collision detection, 401–410 shadows, 411–420 opening, 386 overview, 383–384 ParticleEmitter3D class, 421–435 preloader display, 393–401 score, 411–420 sound, 411–420 terminology, 384 Flash API, 81–84 Flash applications (Google+ API), 518–533 focus (stage), 135–149 Follow button (Twitter), 485 for loops, 58–60 formatting code, 16 frameRate property, 74 frames, troubleshooting target frames, 591–592 frameworks (Flare3D) 3D particle effects, 421–435 astro character, 393–401 cameras, 393–401 cost, 384 Data class, 392 downloading, 384 energy 3D model, 393–401 example directory, 421–435 fans, 393–401 files, 384–386 first-person view, 421–435 GameOverView class, 391–392 GameView class, 389–391 initializing world, 387–392 IntroView class, 388–389 libraries, 384–386 Main class, 387–388 mines, 393–401 objects animating, 393–401 collision detection, 401–410 shadows, 411–420 opening, 386 overview, 383–384 ParticleEmitter3D class, 421–435 preloader display, 393–401 score, 411–420 sound, 411–420 terminology, 384 functions. See also methods callback functions, 205–206 changeRegPt(), 75–76 getLocal(), 76 getTimer(), 57 random(), 68–69 scope, 24–26 shuffle(), 49–50 trace(), 1–2, 11–14 G game view, 95–105 GameView class, 95–105 Android games, 347–349 Flare3D, 389–391 game-over views creating, 149–172 GameOverView class, 95. See also end view Android games, 359–360 Flare3D, 391–392 iOS games, 302–304 GameOverView_Pool class (iOS games), 304–305 GameOverView class, 95. See also end view Android games, 359–360 Flare3D, 391–392 iOS games, 302–304 GameOverView_Pool class (iOS games), 304–305 games 3D (Stage3D/Flare3D) 3D particle effects, 421–435 animating objects, 393–401 astro character, 393–401 cameras, 393–401 collision detection, 401–410 cost, 384 Data class, 392 downloading, 384 energy 3D model, 393–401 example directory, 421–435 fans, 393–401 files, 384–386 first-person view, 421–435 GameOverView class, 391–392 GameView class, 389–391 initializing world, 387–392 IntroView class, 388–389 libraries, 384–386 Main class, 387–388 mines, 393–401 opening, 386 overview, 383–384 ParticleEmitter3D class, 421–435 preloader display, 393–401 score, 411–420 shadows, 411–420 sound, 411–420 terminology, 384 Android. See Android games creating process, 86 iOS. See iOS games multiplayer games. See multiplayer games pausing, 74 registering Facebook, 438–444 Google+, 513–518 restarting, 74 social networks. See social networks states, storing, 76 tank game overview, 85 Twitter, registering, 492–494 Yellow Planet. See Flare3D GameView class, 95–105 Android games, 347–349 Flare3D, 389–391 gc (garbage collection), 57 listeners, 57–58 overview, 57 preparing objects, 135–149 603
604 Index General tab Air for Android, 374–376 Air for iOS, 323–327 geometry adjacent angles, 62 angles of reflection, 62–64 overview, 61–62 Pythagorean Theorem, 64–65 straight angles, 61 sum of triangle’s angles, 61 vertical angles, 62 getLocal() function, 76 getters, 34–39 getTimer() function, 57 Google+ ActionScript 3.0 library, 533–552 API ActionScript, 513 authorizing request, 517–533 JavaScript injection, 517–533 overview, 513 sample Flash applications, 518–533 badges, 509–510 buttons +1, 509 code, 510–513 Share, 510 overview, 508 plug-ins, 510–513 registering games, 513–518 SWC, 551–552 gotoAndPlay method, 591–592 gotoAndStop method, 591–592 GPU usage. See CPU/GPU usage H Hashtag button (Twitter), 485 hit detection. See collision detection hitTestObject() method, 92–93 hitTestPoint() method, 92–93 I Icons tab Air for Android, 380 Air for iOS, 330 Implicit Coercion of a value of Type xxxx to an Unrelated Type yyyy error message, 579 Implicit Coercion of a value with Static Type xxxx to a Possibly Unrelated Type yyyy error message, 579–580 importing classes, 29, 31 injection (javascript), 517–533 input (keyboards), 88–91 instances, tweening, 76–77 internal methods, 31–33, 42–43 internal properties, 31–33 interpolation, linear, 65–67 intializing worlds (Flare3D), 387–392 introduction view, 95–105 IntroView class, 95–105, 172–192 Android games, 342–346 Flare3D, 388–389 iOS games, 262–271 IntroView_Pool class (iOS games), 271–272 IntroView class, 95–105, 172–192 Android games, 342–346 Flare3D, 388–389 iOS games, 262–271 IntroView_Pool class (iOS games), 271–272 iOS games collision detection, 260 CombatView class, 274–279 CombatView_Pool class, 279–280 Controller class, 260, 280–296 Controller_Pool class, 297 Data class, 305–308 display size, 259 distribution certificates, 313, 331–335 distribution provisioning profile, 313, 331–335 EnemyTank class, 301–302 EnemyTank_Pool class, 302 GameOverView class, 302–304 GameOverView_Pool class, 304–305 IntroView class, 262–271 IntroView_Pool class, 271–272 Main class, 260–262 managing memory, 260 mouse, 259 moving tanks, 260 overview, 259–260 PlayerTank class, 299–300 PlayerTank_Pool class, 300–301 publishing Air for iOS Deployment tab, 327–330 Air for iOS General tab, 323–327 Air for iOS Icons tab, 330 Air for iOS Languages tab, 330–331 development certificates, 312–323 development provisioning profile, 312–323 Slider class, 272–274 sliders, 259 sound, 260 Tank class, 298–299 testing, 308–312 Timer class, 297 TimerExt class, 297 touch events, 259 iPad. See iOS games J JavaScript ActionScript communication, 56–57, 445–451, 478 injection (Google+ API), 517–533 JavaScript API (Facebook) ActionScript communication, 446–451 appID, 451–452 Dialogs, 453–466 FB methods, 453 JavaScript SDK, 452 overview, 444–446 permissions, 453 social graph, 466–477 JavaScript SDK (Facebook JavaScript API), 452 K KBcontrols class, 96–105 Keyboard class, 88–89 KeyboardEvent class, 89–91 keyboards event listeners, 89–91, 104–105 input, 88–91 KBcontrols class, 96–105
Index key-press combinations, 110–114 Keyboard class, 88–89 KeyboardEvent class, 89–91 KeyboardType class, 88–89 multiple keydown presses, 135–149 objects, moving, 91–105 repeat delay, 104–110 KeyboardType class, 88–89 keydown presses, multiple, 135–149 key-press combinations, 110–114 L Label Must Be a Simple Identifier error message, 579 Languages tab Air for Android, 381–382 Air for iOS, 330–331 libraries Flare3D, 384–386 Google+ ActionScript 3.0 library, 533–552 Tweetr ActionScript library, 494–508 Light_Pool class (Android games), 366–367 line numbers, debugging, 3 linear interpolation, 65–67 listeners (events) CPU/GPU usage, 235, 256 dispatchers, 38–39, 57–58 keyboards, 89–91, 104–105 managing memory, 211 remote, 235 location. See position logic games. See Android games loops break statements, 60 chunks, 58–60 CPU/GPU usage, 231–233 do, 58–60 enterFrame, 60–61, 124–125 for, 58–60 overview, 58 setInterval(), 60–61 single-multiple comparison, 124–125 Timer, 60–61 troubleshooting lost object references, 592–593 while, 58–60 lost object references, 592–593 M Main class Android games, 339–342 creating, 81–82 Flare3D, 387–388 iOS games, 260–262 organizing, 95–105 peer-to-peer multiplayer games, 554–569 managing CPU/GPU usage. See CPU/GPU usage memory, 197–199 callback functions, 205–206 dispatchEvent, 205–206 display objects, 206 filters, 206 iOS games, 260 listeners, 211 pooling objects, 206–208 reusing objects, 208 sound, 208–210 timers, 210–211 math CPU/GPU usage arithmetic, 212–218 bitwise operators, 212–218 division, 212–213 Math class, 215–218 modulo operator, 214 multiplication, 213–214 geometry adjacent angles, 62 angles of reflection, 62–64 overview, 61–62 Pythagorean Theorem, 64–65 straight angles, 61 sum of triangle’s angles, 61 vertical angles, 62 linear interpolation, 65–67 Math class (CPU/GPU usage), 215–218 modulo operator, 67–68 overview, 61 randomization, 68–69 trigonometry, 69–74 Math class (CPU/GPU usage), 215–218 measuring performance, 194–195 memory. See also performance CPU/GPU usage relationship, 195–197 gc (garbage collection), 57 listeners, 57–58 overview, 57 preparing objects, 135–149 managing, 197–199 callback functions, 205–206 dispatchEvent, 205–206 display objects, 206 filters, 206 iOS games, 260 listeners, 211 pooling objects, 206–208 reusing objects, 208 sound, 208–210 timers, 210–211 tracking, 199–203 usage, 199–203 Mention button (Twitter), 485–486 messages. See error messages methods. See also functions addChild(), 87 FB (Facebook JavaScript API), 453 gotoAndPlay, 591–592 gotoAndStop, 591–592 hitTestObject(), 92–93 hitTestPoint(), 92–93 internal, 31–33, 42–43 private, 31–33, 42–43 protected, 31–33, 42–43 public, 31–33, 37, 42–43 static, 42–43 mines (Flare3D), 393–401 mobile simulators, 308–312 models Flare3D energy 3D model, 393–401 Stage3D. See Stage3D modulo operator (%), 67–68, 214 mouse CPU/GPU usage, 233–235 creating custom cursors, 114–118 events, 74 iOS games, 259 moving, 118–124 rollout events, 118–124 MovieClip class, extending, 85–86 605
606 Index MovieClips MovieClip class, extending, 85–86 scope, 19–23 moving mouse, 118–124 objects angles of reflection, 62–64 boundary violations, 110–114, 149–172 collision detection, 149–172 keyboard, 91–95 keyboards, 96–105 stuttering, 104–110 trigonometry, 69–74 tanks (iOS games), 260 Mudbubble Boy test, 250–255 multiplayer games. See also social networks overview, 553 peer-to-peer Data class, 572–574 Main class, 554–569 NetGroup class, 569–572 overview, 554 P2P class, 569–572 server-based, 553–554 multiple enterFrame loops, compared to single loop, 124–125 keydown presses, 135–149 multiplication (CPU/GPU usage), 213–214 N names classes, 30 variables, 78 NetGroup class, 569–572 No Active Security Context error message, 585 No Default Constructor Found in Base Class xxx error message, 585 notation (arrays), 50–51 numbers errors, 16 randomization, 68–69 Twitter requests per hour limits, 491 O object-oriented programming. See OOP objects associative arrays, 50 collision detection, 51–54, 401–410 color, 51–54 creating, 85, 125–135 Dictionary, 55 display objects, 206 distance, calculating, 64–65 Flare3D animating, 393–401 collision detection, 401–410 shadows, 411–420 gc, preparing, 135–149 linear interpolation, 65–67 managing memory, 206–208 moving angles of reflection, 62–64 boundary violations, 110–114, 149–172 collision detection, 149–172 keyboards, 91–105 stuttering, 104–110 trigonometry, 69–74 OOP (object-oriented programming), 18 class encapsulation, 95–105 overview, 18–19 pooling objects, 206–208 position overlapping, 92–93 randomness, 90–91 properties, 65–67 registration points, 74–76, 85 reusing objects, 208 rotating creating, 114–118 synchronizing, 118–124 scrolling, 65–67 SharedObject, 76 stages, finding, 87–88 tanks. See tanks timelines, 65–67 troubleshooting lost references, 592–593 vectors, 78–79 viewing, overlapping, 87–88 online debugging, 2 OOP (object-oriented programming), 18 class encapsulation, 95–105 overview, 18–19 opening Flare3D, 386 operators (CPU/GPU usage) bitwise operators, 212–218 modulo operator, 67–68, 214 optimizing performance, 193, 203–205 organizing data, 172–192 Main class, 95–105 output, trace() function, 2 overlapping objects position, 92–93 viewing, 87–88 P P2P class, 569–572 packages classes, 29, 31 Flash API, 82–84 Parameter Text Must Be Non-Null error message, 585 parameters, controlling, 172–192 partial blitting (CPU/GPU usage), 228–231 particle effects (Flare3D), 421–435 ParticleEmitter3D class (Flare3D), 421–435 passing variables, 36–37 Patch the Scarecrow test, 247–250 pausing games, 74 peer-to-peer multiplayer games Data class, 572–574 Main class, 554–569 NetGroup class, 569–572 overview, 554 P2P class, 569–572 performance CPU/GPU usage. See CPU/GPU usage development/target platforms, 194–195 measuring, 194–195 memory. See memory optimizing, 193, 203–205 testing, 199–203
Index permissions Air for Android Permissions tab, 380–381 Facebook JavaScript API, 453 Permissions tab (Air for Android), 380–381 permit debugging, 2–8 pixels, 51–54 plane geometry. See geometry platforms (performance), 194–195 PlayerTank class, 96–105. See also tanks creating, 85–86 iOS games, 299–300 PlayerTank_Pool class (iOS games), 300–301 plug-ins (Google+), 510–513 points (registration points), 74–76, 85 pooling objects (managing memory), 206–208 position (objects) overlapping, 92–93 randomness, 90–91 preloader display (Flare3D), 393–401 preparing objects (gc), 135–149 private methods, 31–33, 42–43 private properties, 31–33 process, creating games, 86 profiles custom profiles, debugging, 8–9 publishing iOS games development provisioning profiles, 312–323 distribution provisioning profiles, 313, 331–335 properties cacheAsBitmap, 219–220 cacheAsBitmapMatrix, 219–220 CPU/GPU usage, 219–220 frameRate, 74 getters, 34–39 internal, 31–33 objects, linear interpolation, 65–67 private, 31–33 protected, 31–33 public, 31–33 setters, 34–39 Stage class, 90 static, 39–41 undeclared, assigning, 45–46 protected methods, 31–33, 42–43 protected properties, 31–33 provisioning profiles (publishing iOS games) development provisioning profiles, 312–323 distribution provisioning profiles, 313, 331–335 pseudo classes, 97 public methods, 31–33, 37, 42–43 public properties, 31–33 publishing Android games (Air for Android) Deployment tab, 377–380 General tab, 374–376 Icons tab, 380 Languages tab, 381–382 Permissions tab, 380–381 iOS games (Air for iOS) Deployment tab, 327–330 development certificates, 312–323 development provisioning profile, 312–323 General tab, 323–327 Icons tab, 330 Languages tab, 330–331 puzzle games. See Android games Pythagorean Theorem, 64–65 R random() function, 68–69 randomization, numbers, 68–69 randomness, object position, 90–91 references, lost, 592–593 reflecting (angles of reflection), 62–64 registering games Facebook, 438–444 Google+, 513–518 Twitter, 492–494 registration points (objects), 74–76, 85 remote listeners (CPU/GPU usage), 235 rendering model. See Stage3D repeat delay (keyboards), 104–110 repeatedly executed code, troubleshooting, 594 Replay_Btn class (Android games), 363 requests Google+ API, 517–533 Twitter number per hour limits, 491 that do not require authentication, 486–491 that require authentication, 492–508 restarting games, 74 reusing objects (managing memory), 208 rollout events (mouse movement), 118–124 rotating objects creating, 114–118 synchronizing, 118–124 run-time errors, 3–8 S sample Flash applications (Google+ API), 518–533 saving classes (SWF), 87 scope classes, 23–24 debugging, 17 errors, 17 functions, 24–26 MovieClips, 19–23 overview, 18–19 score (Flare3D), 411–420 Scout, 194–195 scrolling objects, linear interpolation, 65–67 SDKs (JavaScript SDK), 452 security, social networks, 437–438 Security Sandbox Error error message, 586 SecurityError Error #2000 No Active Security Context error message, 585 server-based multiplayer games, 553–554 setInterval() loops, 60–61 setters, 34–39 shadows (Flare3D), 411–420 Share a Link button (Twitter), 484 Share button (Google+), 510 SharedObject object, 76 607
608 Index shooting functionality creating, 114–118 troubleshooting, 149–172 shuffle() function, 49–50 shuffling (arrays), 49–50 significant timeline coding, 17–18 simulators, mobile, 308–312 single enterFrame loops, compared to multiple, 124–125 singletons (classes), 43–45 size display size (iOS games), 259 stages, 90 Slider class Android games, 363–365 iOS games, 272–274 sliders iOS games, 259 Slider class Android games, 363–365 iOS games, 272–274 social games. See multiplayer games; social networks social graph (Facebook JavaScript API), 466–477 social networks Facebook. See Facebook Google+. See Google+ multiplayer games. See multiplayer games overview, 437–438 security, 437–438 Twitter. See Twitter sound creating, 125–135, 149–172 Flare3D, 411–420 iOS games, 260 managing memory, 208–210 stage blitting (CPU/GPU usage), 220–228 focus, troubleshooting, 135–149 Stage class, 90 Stage class, 90 Stage3D CPU/GPU usage overview, 235–236 Starling 10,000 MovieClips test, 242–247 Starling Mudbubble Boy test, 250–255 Starling Patch the Scarecrow test, 247–250 Starling/blitting comparison, 236–256 Flare3D 3D particle effects, 421–435 animating objects, 393–401 astro character, 393–401 cameras, 393–401 collision detection, 401–410 cost, 384 Data class, 392 downloading, 384 energy 3D model, 393–401 example directory, 421–435 fans, 393–401 files, 384–386 first-person view, 421–435 GameOverView class, 391–392 GameView class, 389–391 initializing world, 387–392 IntroView class, 388–389 libraries, 384–386 Main class, 387–388 mines, 393–401 opening, 386 overview, 383–384 ParticleEmitter3D class, 421–435 preloader display, 393–401 score, 411–420 shadows, 411–420 sound, 411–420 terminology, 384 stages finding objects, 87–88 size, 90 stage blitting (CPU/GPU usage), 220–228 tiling, 67–68 Starling 10,000 MovieClips test, 242–247 blitting comparison, 236–256 Mudbubble Boy test, 250–255 Patch the Scarecrow test, 247–250 statements, break, 60 states, storing, 76 static methods, 42–43 static properties, 39–41 statistics displays, 149–172 storing games, states, 76 gc (garbage collection), 57 listeners, 57–58 overview, 57 preparing objects, 135–149 straight angles, 61 strings (arrays), 48–49 strong listeners (managing memory), 211 stuttering objects, 104–110 sum of triangle’s angles, 61 SWC (Google+), 551–552 SWF names, errors, 7–8 saving classes, 87 Switch class (Android games), 362 Switch_Pool class (Android games), 366 Switcher. See Android games symbols (tanks), 125–135 synchronizing, rotating objects, 118–124 T Tank class (iOS games), 298–299 tank game. See games tanks. See also objects drawing, 85 EnemyTank class, 96–105. See also tanks creating, 86, 125–135 iOS games, 301–302 EnemyTank_Pool class (iOS games), 302 moving (iOS games), 260 PlayerTank class, 96–105. See also tanks creating, 85–86 iOS games, 299–300 PlayerTank_Pool class (iOS games), 300–301 shooting creating functionality, 114–118 troubleshooting functionality, 149–172 symbols, 125–135 Tank class (iOS games), 298–299 turrets
Index creating, 114–118 synchronizing, 118–124 target frames, troubleshooting, 591–592 target/development platform comparison, 194–195 templates, debugging, 18 terminology (Flare3D), 384 testing. See also debugging; errors Android games ADB (Android Debug Bridge), 371–372 ADT (Adobe AIR Developer Tool), 372–374 AIR Debug Launcher, 368 emulators, 368–371 overview, 367 experimenting, 14–16 iOS games, 308–312 performance, 199–203 textfields, debugging, 1–2 TF class (Android games), 362–363 TF_Pool class (Android games), 367 The Class xxx Must Subclass yyy Since It Is Linked to a Library Symbol of That Type error message, 586 The Name of Definition Xxxx Does Not Reflect the Location of This File. Please Change the Definition Name inside This File, or Rename the File. error message, 586 The Private Attribute May Be Used Only on Class Property Definitions error message, 578 The SWF file file…someswf.swf Contains Invalid Data error message, 586 tiling (stages), 67–68 time, Twitter requests per hour limits, 491 timelines coding, 17–18 objects, linear interpolation, 65–67 Timer class (iOS games), 297 Timer loops, 60–61 TimerExt class (iOS games), 297 timers managing memory, 210–211 Timer class (iOS games), 297 Timer loops, 60–61 TimerExt class (iOS games), 297 tools, debugging, 1–2 touch events (iOS games), 259 trace() function, 1–2, 11–14 tracking memory, 199–203 triangles Pythagorean Theorem, 64–65 sum of triangle’s angles, 61 trigonometry, 69–74 troubleshooting class files, 595 mouse movement, 118–124 multiple keydown presses, 135–149 shooting functionality, 149–172 stage focus, 135–149 turrets (tanks) creating, 114–118 synchronizing, 118–124 Tween class, 76–77 tweening instances, 76–77 TweenLite class, 76–77 Tweetr ActionScript library, 494–508 Twitter buttons Follow, 485 Hashtag, 485 Mention, 485–486 Share a Link, 484 overview, 482–484 registering games, 492–494 requests number per hour limits, 491 that do not require authentication, 486–491 that require authentication, 492–508 Tweetr ActionScript library, 494–508 Type Was Not Found or Was Not a Compile-Time Constant error message, 578–579 TypeError Error #2007 Parameter Text Must Be Non-Null error message, 585 U undeclared properties, 45–46 Unhandled IOErrorEvent. text=Error #2035 URL Not Found error message, 586 URL Not Found error message, 586 usage (memory), 199–203 users, controlling parameters, 172–192 V variables CPU/GPU usage, 256 names, 78 passing, 36–37 Vector class, 78–79 vectors CPU/GPU usage, 256–258 objects, 78–79 Vector class, 78–79 vertical angles, 62 viewing overlapping objects, 87–88 views/displays creating, 125–135 end view, 95. See also GameOverView class Flare3D first-person view, 421–435 preloader display, 393–401 game view. See game view game-over. See game-over views introduction. See introduction view objects, managing memory, 206 size (iOS games), 259 stage, troubleshooting focus, 135–149 statistics, creating, 149–172 virtual device emulator, 194 W–Y weak listeners (managing memory), 211 while loops, 58–60 worlds (Flare3D), 387–392 Yellow Planet. See Flare3D 609